mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-08-04 23:12:58 +10:00
Merge pull request #48 from wel97459/dev-gps
Added GPS enable and GPS interval settings.
This commit is contained in:
@@ -66,8 +66,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
final Map<String, List<Message>> _conversations = {};
|
||||
final Map<int, List<ChannelMessage>> _channelMessages = {};
|
||||
final Set<String> _loadedConversationKeys = {};
|
||||
final Map<int, Set<String>> _processedChannelReactions = {}; // channelIndex -> Set of "reactionKey_emoji"
|
||||
final Map<String, Set<String>> _processedContactReactions = {}; // contactPubKeyHex -> Set of "reactionKey_emoji"
|
||||
final Map<int, Set<String>> _processedChannelReactions =
|
||||
{}; // channelIndex -> Set of "reactionKey_emoji"
|
||||
final Map<String, Set<String>> _processedContactReactions =
|
||||
{}; // contactPubKeyHex -> Set of "reactionKey_emoji"
|
||||
|
||||
StreamSubscription<List<ScanResult>>? _scanSubscription;
|
||||
StreamSubscription<BluetoothConnectionState>? _connectionSubscription;
|
||||
@@ -108,6 +110,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
int _queueSyncRetries = 0;
|
||||
static const int _maxQueueSyncRetries = 3;
|
||||
static const int _queueSyncTimeoutMs = 5000; // 5 second timeout
|
||||
Map<String, String>? _currentCustomVars;
|
||||
|
||||
// Channel syncing state (sequential pattern)
|
||||
bool _isSyncingChannels = false;
|
||||
@@ -137,7 +140,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
final ContactStore _contactStore = ContactStore();
|
||||
final UnreadStore _unreadStore = UnreadStore();
|
||||
final Map<int, bool> _channelSmazEnabled = {};
|
||||
bool _lastSentWasCliCommand = false; // Track if last sent message was a CLI command
|
||||
bool _lastSentWasCliCommand =
|
||||
false; // Track if last sent message was a CLI command
|
||||
final Map<String, bool> _contactSmazEnabled = {};
|
||||
final Set<String> _knownContactKeys = {};
|
||||
final Map<String, int> _contactLastReadMs = {};
|
||||
@@ -166,6 +170,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
return 'Unknown Device';
|
||||
}
|
||||
|
||||
List<ScanResult> get scanResults => List.unmodifiable(_scanResults);
|
||||
List<Contact> get contacts {
|
||||
final selfKey = _selfPublicKey;
|
||||
@@ -176,6 +181,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_contacts.where((contact) => !listEquals(contact.publicKey, selfKey)),
|
||||
);
|
||||
}
|
||||
|
||||
List<Channel> get channels => List.unmodifiable(_channels);
|
||||
bool get isConnected => _state == MeshCoreConnectionState.connected;
|
||||
bool get isLoadingContacts => _isLoadingContacts;
|
||||
@@ -191,12 +197,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
int? get currentBwHz => _currentBwHz;
|
||||
int? get currentSf => _currentSf;
|
||||
int? get currentCr => _currentCr;
|
||||
Map<String, String>? get currentCustomVars => _currentCustomVars;
|
||||
int? get batteryMillivolts => _batteryMillivolts;
|
||||
int get maxContacts => _maxContacts;
|
||||
int get maxChannels => _maxChannels;
|
||||
bool get isSyncingQueuedMessages => _isSyncingQueuedMessages;
|
||||
bool get isSyncingChannels => _isSyncingChannels;
|
||||
int get channelSyncProgress => _isSyncingChannels && _totalChannelsToRequest > 0
|
||||
int get channelSyncProgress =>
|
||||
_isSyncingChannels && _totalChannelsToRequest > 0
|
||||
? ((_nextChannelIndexToRequest / _totalChannelsToRequest) * 100).round()
|
||||
: 0;
|
||||
int? get batteryPercent => _batteryMillivolts == null
|
||||
@@ -377,7 +385,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void setActiveContact(String? contactKeyHex) {
|
||||
if (contactKeyHex != null && !_shouldTrackUnreadForContactKey(contactKeyHex)) {
|
||||
if (contactKeyHex != null &&
|
||||
!_shouldTrackUnreadForContactKey(contactKeyHex)) {
|
||||
_activeContactKey = null;
|
||||
return;
|
||||
}
|
||||
@@ -429,7 +438,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
/// Load persisted channel messages for a specific channel
|
||||
Future<void> _loadChannelMessages(int channelIndex) async {
|
||||
final allMessages = await _channelMessageStore.loadChannelMessages(channelIndex);
|
||||
final allMessages = await _channelMessageStore.loadChannelMessages(
|
||||
channelIndex,
|
||||
);
|
||||
if (allMessages.isNotEmpty) {
|
||||
// Keep only the most recent N messages in memory to bound memory usage
|
||||
final windowedMessages = allMessages.length > _messageWindowSize
|
||||
@@ -446,7 +457,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
int channelIndex, {
|
||||
int count = 50,
|
||||
}) async {
|
||||
final allMessages = await _channelMessageStore.loadChannelMessages(channelIndex);
|
||||
final allMessages = await _channelMessageStore.loadChannelMessages(
|
||||
channelIndex,
|
||||
);
|
||||
final currentMessages = _channelMessages[channelIndex] ?? [];
|
||||
|
||||
if (allMessages.length <= currentMessages.length) {
|
||||
@@ -551,7 +564,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
final contactKey = pubKeyToHex(message.senderKey);
|
||||
final messages = _conversations[contactKey];
|
||||
if (messages != null) {
|
||||
final index = messages.indexWhere((m) => m.messageId == message.messageId);
|
||||
final index = messages.indexWhere(
|
||||
(m) => m.messageId == message.messageId,
|
||||
);
|
||||
if (index != -1) {
|
||||
messages[index] = message;
|
||||
_messageStore.saveMessages(contactKey, messages);
|
||||
@@ -576,7 +591,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Contact _applyAutoSelection(Contact contact, PathSelection? selection) {
|
||||
if (selection == null || selection.useFlood || selection.pathBytes.isEmpty) {
|
||||
if (selection == null ||
|
||||
selection.useFlood ||
|
||||
selection.pathBytes.isEmpty) {
|
||||
return contact;
|
||||
}
|
||||
|
||||
@@ -584,7 +601,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
publicKey: contact.publicKey,
|
||||
name: contact.name,
|
||||
type: contact.type,
|
||||
pathLength: selection.hopCount >= 0 ? selection.hopCount : contact.pathLength,
|
||||
pathLength: selection.hopCount >= 0
|
||||
? selection.hopCount
|
||||
: contact.pathLength,
|
||||
path: Uint8List.fromList(selection.pathBytes),
|
||||
latitude: contact.latitude,
|
||||
longitude: contact.longitude,
|
||||
@@ -593,7 +612,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> startScan({Duration timeout = const Duration(seconds: 10)}) async {
|
||||
Future<void> startScan({
|
||||
Duration timeout = const Duration(seconds: 10),
|
||||
}) async {
|
||||
if (_state == MeshCoreConnectionState.scanning) return;
|
||||
|
||||
_scanResults.clear();
|
||||
@@ -714,7 +735,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
if (attempt == 2) rethrow;
|
||||
}
|
||||
}
|
||||
_notifySubscription = _txCharacteristic!.onValueReceived.listen(_handleFrame);
|
||||
_notifySubscription = _txCharacteristic!.onValueReceived.listen(
|
||||
_handleFrame,
|
||||
);
|
||||
|
||||
_setState(MeshCoreConnectionState.connected);
|
||||
|
||||
@@ -771,8 +794,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
return result;
|
||||
}
|
||||
|
||||
bool get _shouldAutoReconnect =>
|
||||
!_manualDisconnect && _lastDeviceId != null;
|
||||
bool get _shouldAutoReconnect => !_manualDisconnect && _lastDeviceId != null;
|
||||
|
||||
void _cancelReconnectTimer() {
|
||||
_reconnectTimer?.cancel();
|
||||
@@ -799,7 +821,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
final device = _lastDevice ??
|
||||
final device =
|
||||
_lastDevice ??
|
||||
(_lastDeviceId == null
|
||||
? null
|
||||
: BluetoothDevice.fromId(_lastDeviceId!));
|
||||
@@ -931,6 +954,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
await sendFrame(buildAppStartFrame());
|
||||
await requestBatteryStatus(force: true);
|
||||
await sendFrame(buildGetRadioSettingsFrame());
|
||||
await sendFrame(buildGetCustomVarsFrame());
|
||||
_scheduleSelfInfoRetry();
|
||||
}
|
||||
|
||||
@@ -938,6 +962,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_awaitingSelfInfo = true;
|
||||
await sendFrame(buildDeviceQueryFrame());
|
||||
await sendFrame(buildAppStartFrame());
|
||||
await sendFrame(buildGetCustomVarsFrame());
|
||||
await requestBatteryStatus();
|
||||
|
||||
_scheduleSelfInfoRetry();
|
||||
@@ -945,9 +970,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
void _scheduleSelfInfoRetry() {
|
||||
_selfInfoRetryTimer?.cancel();
|
||||
_selfInfoRetryTimer = Timer.periodic(
|
||||
const Duration(milliseconds: 3500),
|
||||
(timer) {
|
||||
_selfInfoRetryTimer = Timer.periodic(const Duration(milliseconds: 3500), (
|
||||
timer,
|
||||
) {
|
||||
if (!isConnected) {
|
||||
timer.cancel();
|
||||
return;
|
||||
@@ -957,8 +982,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
unawaited(sendFrame(buildAppStartFrame()));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> getContacts({int? since, bool preserveExisting = false}) async {
|
||||
@@ -979,10 +1003,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> refreshContactsSinceLastmod() async {
|
||||
await getContacts(
|
||||
since: _latestContactLastmod(),
|
||||
preserveExisting: true,
|
||||
);
|
||||
await getContacts(since: _latestContactLastmod(), preserveExisting: true);
|
||||
}
|
||||
|
||||
Future<void> getContactByKey(Uint8List pubKey) async {
|
||||
@@ -990,18 +1011,20 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
await sendFrame(buildGetContactByKeyFrame(pubKey));
|
||||
}
|
||||
|
||||
Future<void> sendMessage(
|
||||
Contact contact,
|
||||
String text,
|
||||
) async {
|
||||
Future<void> sendMessage(Contact contact, String text) async {
|
||||
if (!isConnected || text.isEmpty) return;
|
||||
|
||||
// Handle auto-rotation if enabled
|
||||
PathSelection? autoSelection;
|
||||
if (_appSettingsService?.settings.autoRouteRotationEnabled == true) {
|
||||
autoSelection = _pathHistoryService?.getNextAutoPathSelection(contact.publicKeyHex);
|
||||
autoSelection = _pathHistoryService?.getNextAutoPathSelection(
|
||||
contact.publicKeyHex,
|
||||
);
|
||||
if (autoSelection != null) {
|
||||
_pathHistoryService?.recordPathAttempt(contact.publicKeyHex, autoSelection);
|
||||
_pathHistoryService?.recordPathAttempt(
|
||||
contact.publicKeyHex,
|
||||
autoSelection,
|
||||
);
|
||||
if (!autoSelection.useFlood && autoSelection.pathBytes.isNotEmpty) {
|
||||
await setContactPath(
|
||||
contact,
|
||||
@@ -1036,12 +1059,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_addMessage(contact.publicKeyHex, message);
|
||||
notifyListeners();
|
||||
final outboundText = prepareContactOutboundText(contact, text);
|
||||
await sendFrame(
|
||||
buildSendTextMsgFrame(
|
||||
contact.publicKey,
|
||||
outboundText,
|
||||
),
|
||||
);
|
||||
await sendFrame(buildSendTextMsgFrame(contact.publicKey, outboundText));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1052,13 +1070,15 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
) async {
|
||||
if (!isConnected) return;
|
||||
|
||||
await sendFrame(buildUpdateContactPathFrame(
|
||||
await sendFrame(
|
||||
buildUpdateContactPathFrame(
|
||||
contact.publicKey,
|
||||
customPath,
|
||||
pathLen,
|
||||
type: contact.type,
|
||||
name: contact.name,
|
||||
));
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Set path override for a contact (persists across contact refreshes)
|
||||
@@ -1068,16 +1088,27 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
int? pathLen,
|
||||
Uint8List? pathBytes,
|
||||
}) async {
|
||||
appLogger.info('setPathOverride called for ${contact.name}: pathLen=$pathLen, bytesLen=${pathBytes?.length ?? 0}', tag: 'Connector');
|
||||
appLogger.info(
|
||||
'setPathOverride called for ${contact.name}: pathLen=$pathLen, bytesLen=${pathBytes?.length ?? 0}',
|
||||
tag: 'Connector',
|
||||
);
|
||||
|
||||
// Find contact in list
|
||||
final index = _contacts.indexWhere((c) => c.publicKeyHex == contact.publicKeyHex);
|
||||
final index = _contacts.indexWhere(
|
||||
(c) => c.publicKeyHex == contact.publicKeyHex,
|
||||
);
|
||||
if (index == -1) {
|
||||
appLogger.warn('setPathOverride: Contact not found in list: ${contact.name}', tag: 'Connector');
|
||||
appLogger.warn(
|
||||
'setPathOverride: Contact not found in list: ${contact.name}',
|
||||
tag: 'Connector',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
appLogger.info('Found contact at index $index. Current override: ${_contacts[index].pathOverride}', tag: 'Connector');
|
||||
appLogger.info(
|
||||
'Found contact at index $index. Current override: ${_contacts[index].pathOverride}',
|
||||
tag: 'Connector',
|
||||
);
|
||||
|
||||
// Update contact with new path override
|
||||
_contacts[index] = _contacts[index].copyWith(
|
||||
@@ -1086,7 +1117,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
clearPathOverride: pathLen == null, // Clear if pathLen is null
|
||||
);
|
||||
|
||||
appLogger.info('Updated contact. New override: ${_contacts[index].pathOverride}, bytesLen: ${_contacts[index].pathOverrideBytes?.length}', tag: 'Connector');
|
||||
appLogger.info(
|
||||
'Updated contact. New override: ${_contacts[index].pathOverride}, bytesLen: ${_contacts[index].pathOverrideBytes?.length}',
|
||||
tag: 'Connector',
|
||||
);
|
||||
|
||||
// Save to storage
|
||||
await _contactStore.saveContacts(_contacts);
|
||||
@@ -1099,7 +1133,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
appLogger.info('Path sent to device', tag: 'Connector');
|
||||
}
|
||||
|
||||
debugPrint('Set path override for ${contact.name}: pathLen=$pathLen, bytes=${pathBytes?.length ?? 0}');
|
||||
debugPrint(
|
||||
'Set path override for ${contact.name}: pathLen=$pathLen, bytes=${pathBytes?.length ?? 0}',
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -1148,7 +1184,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
outboundText,
|
||||
selfKey,
|
||||
);
|
||||
final ackHashHex = ackHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
||||
final ackHashHex = ackHash
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
final messageBytes = utf8.encode(outboundText).length;
|
||||
_pendingRepeaterAcks[ackHashHex]?.timeout?.cancel();
|
||||
_pendingRepeaterAcks[ackHashHex] = _RepeaterAckContext(
|
||||
@@ -1215,9 +1253,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
// Check if we've already processed this reaction
|
||||
_processedChannelReactions.putIfAbsent(channel.index, () => {});
|
||||
final reactionKey = reactionInfo.reactionKey;
|
||||
final reactionIdentifier = reactionKey != null ? '${reactionKey}_${reactionInfo.emoji}' : null;
|
||||
final reactionIdentifier = reactionKey != null
|
||||
? '${reactionKey}_${reactionInfo.emoji}'
|
||||
: null;
|
||||
|
||||
if (reactionIdentifier != null && _processedChannelReactions[channel.index]!.contains(reactionIdentifier)) {
|
||||
if (reactionIdentifier != null &&
|
||||
_processedChannelReactions[channel.index]!.contains(
|
||||
reactionIdentifier,
|
||||
)) {
|
||||
// Already processed, don't process again
|
||||
return;
|
||||
}
|
||||
@@ -1242,13 +1285,19 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
final message = ChannelMessage.outgoing(text, _selfName ?? 'Me', channel.index);
|
||||
final message = ChannelMessage.outgoing(
|
||||
text,
|
||||
_selfName ?? 'Me',
|
||||
channel.index,
|
||||
);
|
||||
_addChannelMessage(channel.index, message);
|
||||
notifyListeners();
|
||||
|
||||
final trimmed = text.trim();
|
||||
final isStructuredPayload = trimmed.startsWith('g:') || trimmed.startsWith('m:');
|
||||
final outboundText = (isChannelSmazEnabled(channel.index) && !isStructuredPayload)
|
||||
final isStructuredPayload =
|
||||
trimmed.startsWith('g:') || trimmed.startsWith('m:');
|
||||
final outboundText =
|
||||
(isChannelSmazEnabled(channel.index) && !isStructuredPayload)
|
||||
? Smaz.encodeIfSmaller(text)
|
||||
: text;
|
||||
await sendFrame(buildSendChannelTextMsgFrame(channel.index, outboundText));
|
||||
@@ -1264,9 +1313,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_conversations.remove(contact.publicKeyHex);
|
||||
_loadedConversationKeys.remove(contact.publicKeyHex);
|
||||
_contactLastReadMs.remove(contact.publicKeyHex);
|
||||
_unreadStore.saveContactLastRead(
|
||||
Map<String, int>.from(_contactLastReadMs),
|
||||
);
|
||||
_unreadStore.saveContactLastRead(Map<String, int>.from(_contactLastReadMs));
|
||||
_messageStore.clearMessages(contact.publicKeyHex);
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -1275,8 +1322,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
if (!isConnected) return;
|
||||
|
||||
await sendFrame(buildResetPathFrame(contact.publicKey));
|
||||
final existingIndex =
|
||||
_contacts.indexWhere((c) => c.publicKeyHex == contact.publicKeyHex);
|
||||
final existingIndex = _contacts.indexWhere(
|
||||
(c) => c.publicKeyHex == contact.publicKeyHex,
|
||||
);
|
||||
if (existingIndex >= 0) {
|
||||
final existing = _contacts[existingIndex];
|
||||
// Use copyWith to preserve pathOverride and pathOverrideBytes
|
||||
@@ -1295,8 +1343,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
Uint8List? pathBytes,
|
||||
int? pathLength,
|
||||
}) {
|
||||
final existingIndex =
|
||||
_contacts.indexWhere((c) => c.publicKeyHex == publicKeyHex);
|
||||
final existingIndex = _contacts.indexWhere(
|
||||
(c) => c.publicKeyHex == publicKeyHex,
|
||||
);
|
||||
if (existingIndex >= 0) {
|
||||
final existing = _contacts[existingIndex];
|
||||
_contacts[existingIndex] = existing.copyWith(
|
||||
@@ -1344,7 +1393,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_handleQueueSyncTimeout();
|
||||
});
|
||||
|
||||
debugPrint('[QueueSync] Requesting next message (retry: $_queueSyncRetries/$_maxQueueSyncRetries)');
|
||||
debugPrint(
|
||||
'[QueueSync] Requesting next message (retry: $_queueSyncRetries/$_maxQueueSyncRetries)',
|
||||
);
|
||||
|
||||
try {
|
||||
await sendFrame(buildSyncNextMessageFrame());
|
||||
@@ -1358,7 +1409,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void _handleQueueSyncTimeout() {
|
||||
debugPrint('[QueueSync] Timeout waiting for message (retry: $_queueSyncRetries/$_maxQueueSyncRetries)');
|
||||
debugPrint(
|
||||
'[QueueSync] Timeout waiting for message (retry: $_queueSyncRetries/$_maxQueueSyncRetries)',
|
||||
);
|
||||
|
||||
if (_queueSyncRetries < _maxQueueSyncRetries) {
|
||||
// Retry
|
||||
@@ -1389,11 +1442,19 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
await sendFrame(buildSetAdvertNameFrame(name));
|
||||
}
|
||||
|
||||
Future<void> setNodeLocation({required double lat, required double lon}) async {
|
||||
Future<void> setNodeLocation({
|
||||
required double lat,
|
||||
required double lon,
|
||||
}) async {
|
||||
if (!isConnected) return;
|
||||
await sendFrame(buildSetAdvertLatLonFrame(lat, lon));
|
||||
}
|
||||
|
||||
Future<void> setCustomVar(String value) async {
|
||||
if (!isConnected) return;
|
||||
await sendFrame(buildSetCustomVarFrame(value));
|
||||
}
|
||||
|
||||
Future<void> sendSelfAdvert({bool flood = true}) async {
|
||||
if (!isConnected) return;
|
||||
await sendFrame(buildSendSelfAdvertFrame(flood: flood));
|
||||
@@ -1424,7 +1485,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_channelSyncRetries = 0;
|
||||
notifyListeners();
|
||||
|
||||
debugPrint('[ChannelSync] Starting sync for $_totalChannelsToRequest channels');
|
||||
debugPrint(
|
||||
'[ChannelSync] Starting sync for $_totalChannelsToRequest channels',
|
||||
);
|
||||
|
||||
// Start sequential sync
|
||||
await _requestNextChannel();
|
||||
@@ -1456,7 +1519,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
() => _handleChannelSyncTimeout(channelIndex),
|
||||
);
|
||||
|
||||
debugPrint('[ChannelSync] Requesting channel $channelIndex/$_totalChannelsToRequest (retry: $_channelSyncRetries/$_maxChannelSyncRetries)');
|
||||
debugPrint(
|
||||
'[ChannelSync] Requesting channel $channelIndex/$_totalChannelsToRequest (retry: $_channelSyncRetries/$_maxChannelSyncRetries)',
|
||||
);
|
||||
|
||||
try {
|
||||
await sendFrame(buildGetChannelFrame(channelIndex));
|
||||
@@ -1468,7 +1533,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void _handleChannelSyncTimeout(int channelIndex) {
|
||||
debugPrint('[ChannelSync] Timeout waiting for channel $channelIndex (retry: $_channelSyncRetries/$_maxChannelSyncRetries)');
|
||||
debugPrint(
|
||||
'[ChannelSync] Timeout waiting for channel $channelIndex (retry: $_channelSyncRetries/$_maxChannelSyncRetries)',
|
||||
);
|
||||
|
||||
if (_channelSyncRetries < _maxChannelSyncRetries) {
|
||||
// Retry the same channel
|
||||
@@ -1477,16 +1544,20 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
unawaited(_requestNextChannel());
|
||||
} else {
|
||||
// Max retries reached for this channel, restore from cache and move to next
|
||||
debugPrint('[ChannelSync] Max retries reached for channel $channelIndex, attempting cache restore');
|
||||
debugPrint(
|
||||
'[ChannelSync] Max retries reached for channel $channelIndex, attempting cache restore',
|
||||
);
|
||||
|
||||
// Try to restore this channel from cache
|
||||
try {
|
||||
final cachedChannel = _previousChannelsCache.firstWhere(
|
||||
(c) => c.index == channelIndex
|
||||
(c) => c.index == channelIndex,
|
||||
);
|
||||
if (!cachedChannel.isEmpty) {
|
||||
_channels.add(cachedChannel);
|
||||
debugPrint('[ChannelSync] Restored channel $channelIndex (${cachedChannel.name}) from cache');
|
||||
debugPrint(
|
||||
'[ChannelSync] Restored channel $channelIndex (${cachedChannel.name}) from cache',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// No cached channel found, that's okay
|
||||
@@ -1503,7 +1574,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
void _completeChannelSync() {
|
||||
_channelSyncTimeout?.cancel();
|
||||
|
||||
debugPrint('[ChannelSync] Sync complete: received ${_channels.length}/$_totalChannelsToRequest channels');
|
||||
debugPrint(
|
||||
'[ChannelSync] Sync complete: received ${_channels.length}/$_totalChannelsToRequest channels',
|
||||
);
|
||||
|
||||
_cleanupChannelSync(completed: true);
|
||||
|
||||
@@ -1541,9 +1614,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
// Delete by setting empty name and zero PSK
|
||||
await sendFrame(buildSetChannelFrame(index, '', Uint8List(16)));
|
||||
_channelLastReadMs.remove(index);
|
||||
_unreadStore.saveChannelLastRead(
|
||||
Map<int, int>.from(_channelLastReadMs),
|
||||
);
|
||||
_unreadStore.saveChannelLastRead(Map<int, int>.from(_channelLastReadMs));
|
||||
// Refresh channels after deleting
|
||||
await getChannels();
|
||||
}
|
||||
@@ -1629,6 +1700,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
case respCodeBattAndStorage:
|
||||
_handleBatteryAndStorage(frame);
|
||||
break;
|
||||
case respCodeCustomVars:
|
||||
_handleCustomVars(frame);
|
||||
default:
|
||||
debugPrint('Unknown frame code: $code');
|
||||
}
|
||||
@@ -1705,8 +1778,12 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
// Firmware reports MAX_CONTACTS / 2 for v3+ device info.
|
||||
final reportedContacts = frame[2];
|
||||
final reportedChannels = frame[3];
|
||||
final nextMaxContacts = reportedContacts > 0 ? reportedContacts * 2 : _maxContacts;
|
||||
final nextMaxChannels = reportedChannels > 0 ? reportedChannels : _maxChannels;
|
||||
final nextMaxContacts = reportedContacts > 0
|
||||
? reportedContacts * 2
|
||||
: _maxContacts;
|
||||
final nextMaxChannels = reportedChannels > 0
|
||||
? reportedChannels
|
||||
: _maxChannels;
|
||||
final previousMaxChannels = _maxChannels;
|
||||
if (nextMaxContacts != _maxContacts || nextMaxChannels != _maxChannels) {
|
||||
_maxContacts = nextMaxContacts;
|
||||
@@ -1751,7 +1828,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_currentBwHz = readUint32LE(frame, 5);
|
||||
_currentSf = frame[9];
|
||||
_currentCr = frame[10];
|
||||
debugPrint('Radio settings: freq=$_currentFreqHz bw=$_currentBwHz sf=$_currentSf cr=$_currentCr');
|
||||
debugPrint(
|
||||
'Radio settings: freq=$_currentFreqHz bw=$_currentBwHz sf=$_currentSf cr=$_currentCr',
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -1822,11 +1901,15 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
final existing = _contacts[existingIndex];
|
||||
final mergedLastMessageAt = existing.lastMessageAt.isAfter(contact.lastMessageAt)
|
||||
final mergedLastMessageAt =
|
||||
existing.lastMessageAt.isAfter(contact.lastMessageAt)
|
||||
? existing.lastMessageAt
|
||||
: contact.lastMessageAt;
|
||||
|
||||
appLogger.info('Refreshing contact ${contact.name}: devicePath=${contact.pathLength}, existingOverride=${existing.pathOverride}', tag: 'Connector');
|
||||
appLogger.info(
|
||||
'Refreshing contact ${contact.name}: devicePath=${contact.pathLength}, existingOverride=${existing.pathOverride}',
|
||||
tag: 'Connector',
|
||||
);
|
||||
|
||||
// CRITICAL: Preserve user's path override when contact is refreshed from device
|
||||
_contacts[existingIndex] = contact.copyWith(
|
||||
@@ -1835,10 +1918,16 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
pathOverrideBytes: existing.pathOverrideBytes,
|
||||
);
|
||||
|
||||
appLogger.info('After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}', tag: 'Connector');
|
||||
appLogger.info(
|
||||
'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}',
|
||||
tag: 'Connector',
|
||||
);
|
||||
} else {
|
||||
_contacts.add(contact);
|
||||
appLogger.info('Added new contact ${contact.name}: pathLen=${contact.pathLength}', tag: 'Connector');
|
||||
appLogger.info(
|
||||
'Added new contact ${contact.name}: pathLen=${contact.pathLength}',
|
||||
tag: 'Connector',
|
||||
);
|
||||
}
|
||||
_knownContactKeys.add(contact.publicKeyHex);
|
||||
_loadMessagesForContact(contact.publicKeyHex);
|
||||
@@ -1970,9 +2059,13 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
if (message == null && !_isLoadingContacts) {
|
||||
final senderPrefix = _extractSenderPrefix(frame);
|
||||
if (senderPrefix != null) {
|
||||
final hasContact = _contacts.any((c) => _matchesPrefix(c.publicKey, senderPrefix));
|
||||
final hasContact = _contacts.any(
|
||||
(c) => _matchesPrefix(c.publicKey, senderPrefix),
|
||||
);
|
||||
if (!hasContact) {
|
||||
debugPrint('Received message from unknown contact, refreshing contacts...');
|
||||
debugPrint(
|
||||
'Received message from unknown contact, refreshing contacts...',
|
||||
);
|
||||
await refreshContactsSinceLastmod();
|
||||
// Retry parsing after refresh
|
||||
message = _parseContactMessage(frame);
|
||||
@@ -2017,7 +2110,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
// Show notification for new incoming message
|
||||
if (!message.isOutgoing && !message.isCli && _appSettingsService != null) {
|
||||
if (!message.isOutgoing &&
|
||||
!message.isCli &&
|
||||
_appSettingsService != null) {
|
||||
final settings = _appSettingsService!.settings;
|
||||
if (settings.notificationsEnabled && settings.notifyOnNewMessage) {
|
||||
// Find the contact name
|
||||
@@ -2074,9 +2169,17 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
// Try base text offset; if empty and there is room for the optional 4-byte extra
|
||||
// (used by signed/plain variants), try again skipping those bytes.
|
||||
var text = readCString(frame, baseTextOffset, frame.length - baseTextOffset);
|
||||
var text = readCString(
|
||||
frame,
|
||||
baseTextOffset,
|
||||
frame.length - baseTextOffset,
|
||||
);
|
||||
if (text.isEmpty && frame.length > baseTextOffset + 4) {
|
||||
text = readCString(frame, baseTextOffset + 4, frame.length - (baseTextOffset + 4));
|
||||
text = readCString(
|
||||
frame,
|
||||
baseTextOffset + 4,
|
||||
frame.length - (baseTextOffset + 4),
|
||||
);
|
||||
}
|
||||
if (text.isEmpty) return null;
|
||||
final decodedText = isCli ? text : (Smaz.tryDecodePrefixed(text) ?? text);
|
||||
@@ -2099,7 +2202,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
status: MessageStatus.delivered,
|
||||
pathLength: pathLenByte == 0xFF ? 0 : pathLenByte,
|
||||
pathBytes: Uint8List(0),
|
||||
fourByteRoomContactKey: fourBytePubMSG
|
||||
fourByteRoomContactKey: fourBytePubMSG,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2140,17 +2243,15 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
String prepareContactOutboundText(Contact contact, String text) {
|
||||
final trimmed = text.trim();
|
||||
final isStructuredPayload =
|
||||
trimmed.startsWith('g:') || trimmed.startsWith('m:') || trimmed.startsWith('V1|');
|
||||
trimmed.startsWith('g:') ||
|
||||
trimmed.startsWith('m:') ||
|
||||
trimmed.startsWith('V1|');
|
||||
if (!isStructuredPayload && isContactSmazEnabled(contact.publicKeyHex)) {
|
||||
return Smaz.encodeIfSmaller(text);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
String _channelDisplayName(int channelIndex) {
|
||||
for (final channel in _channels) {
|
||||
if (channel.index != channelIndex) continue;
|
||||
@@ -2184,7 +2285,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
void _handleIncomingChannelMessage(Uint8List frame) {
|
||||
final message = ChannelMessage.fromFrame(frame);
|
||||
if (message != null && message.channelIndex != null) {
|
||||
if (_shouldDropSelfChannelMessage(message.senderName, message.pathBytes)) {
|
||||
if (_shouldDropSelfChannelMessage(
|
||||
message.senderName,
|
||||
message.pathBytes,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
_updateContactLastMessageAtByName(
|
||||
@@ -2257,7 +2361,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_maybeMarkActiveChannelRead(message);
|
||||
notifyListeners();
|
||||
if (isNew) {
|
||||
final label = channel.name.isEmpty ? 'Channel ${channel.index}' : channel.name;
|
||||
final label = channel.name.isEmpty
|
||||
? 'Channel ${channel.index}'
|
||||
: channel.name;
|
||||
_maybeNotifyChannelMessage(message, channelName: label);
|
||||
}
|
||||
return;
|
||||
@@ -2277,7 +2383,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
// Check if this is a CLI command ACK - if so, ignore it
|
||||
if (_lastSentWasCliCommand) {
|
||||
final ackHashHex = ackHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
||||
final ackHashHex = ackHash
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
debugPrint('Ignoring CLI command ACK (sent): $ackHashHex');
|
||||
_lastSentWasCliCommand = false;
|
||||
return;
|
||||
@@ -2294,7 +2402,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
// Fallback to old behavior
|
||||
for (var messages in _conversations.values) {
|
||||
for (int i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].isOutgoing && messages[i].status == MessageStatus.pending) {
|
||||
if (messages[i].isOutgoing &&
|
||||
messages[i].status == MessageStatus.pending) {
|
||||
messages[i] = messages[i].copyWith(status: MessageStatus.sent);
|
||||
notifyListeners();
|
||||
return;
|
||||
@@ -2328,7 +2437,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
// Fallback to old behavior
|
||||
for (var messages in _conversations.values) {
|
||||
for (int i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].isOutgoing && messages[i].status == MessageStatus.sent) {
|
||||
if (messages[i].isOutgoing &&
|
||||
messages[i].status == MessageStatus.sent) {
|
||||
messages[i] = messages[i].copyWith(status: MessageStatus.delivered);
|
||||
notifyListeners();
|
||||
return;
|
||||
@@ -2339,7 +2449,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
|
||||
bool _handleRepeaterCommandSent(Uint8List ackHash, int timeoutMs) {
|
||||
final ackHashHex = ackHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
||||
final ackHashHex = ackHash
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
final entry = _pendingRepeaterAcks[ackHashHex];
|
||||
if (entry == null) return false;
|
||||
|
||||
@@ -2358,7 +2470,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
|
||||
bool _handleRepeaterCommandAck(Uint8List ackHash, int tripTimeMs) {
|
||||
final ackHashHex = ackHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
||||
final ackHashHex = ackHash
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
final entry = _pendingRepeaterAcks.remove(ackHashHex);
|
||||
if (entry == null) return false;
|
||||
entry.timeout?.cancel();
|
||||
@@ -2370,7 +2484,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
final channel = Channel.fromFrame(frame);
|
||||
if (channel == null) return;
|
||||
|
||||
debugPrint('[ChannelSync] Received channel ${channel.index}: ${channel.isEmpty ? "empty" : channel.name}');
|
||||
debugPrint(
|
||||
'[ChannelSync] Received channel ${channel.index}: ${channel.isEmpty ? "empty" : channel.name}',
|
||||
);
|
||||
|
||||
// If we're syncing and this is the channel we're waiting for
|
||||
if (_isSyncingChannels && _channelSyncInFlight) {
|
||||
@@ -2392,9 +2508,12 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
} else {
|
||||
// Received a channel but not the one we're waiting for
|
||||
// This can happen if device sends unsolicited updates
|
||||
debugPrint('[ChannelSync] Received unexpected channel ${channel.index}, expected $_nextChannelIndexToRequest');
|
||||
debugPrint(
|
||||
'[ChannelSync] Received unexpected channel ${channel.index}, expected $_nextChannelIndexToRequest',
|
||||
);
|
||||
// Add it anyway but don't advance sync
|
||||
if (!channel.isEmpty && !_channels.any((c) => c.index == channel.index)) {
|
||||
if (!channel.isEmpty &&
|
||||
!_channels.any((c) => c.index == channel.index)) {
|
||||
_channels.add(channel);
|
||||
}
|
||||
return;
|
||||
@@ -2404,7 +2523,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
// Not syncing, or received unsolicited update - handle normally
|
||||
if (!channel.isEmpty) {
|
||||
// Update or add channel
|
||||
final existingIndex = _channels.indexWhere((c) => c.index == channel.index);
|
||||
final existingIndex = _channels.indexWhere(
|
||||
(c) => c.index == channel.index,
|
||||
);
|
||||
if (existingIndex >= 0) {
|
||||
_channels[existingIndex] = channel;
|
||||
} else {
|
||||
@@ -2469,26 +2590,30 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
return latestMs;
|
||||
}
|
||||
|
||||
void _setContactLastReadMs(String contactKeyHex, int timestampMs, {bool notify = true}) {
|
||||
void _setContactLastReadMs(
|
||||
String contactKeyHex,
|
||||
int timestampMs, {
|
||||
bool notify = true,
|
||||
}) {
|
||||
if (!_shouldTrackUnreadForContactKey(contactKeyHex)) return;
|
||||
final existing = _contactLastReadMs[contactKeyHex] ?? 0;
|
||||
if (timestampMs <= existing) return;
|
||||
_contactLastReadMs[contactKeyHex] = timestampMs;
|
||||
_unreadStore.saveContactLastRead(
|
||||
Map<String, int>.from(_contactLastReadMs),
|
||||
);
|
||||
_unreadStore.saveContactLastRead(Map<String, int>.from(_contactLastReadMs));
|
||||
if (notify) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void _setChannelLastReadMs(int channelIndex, int timestampMs, {bool notify = true}) {
|
||||
void _setChannelLastReadMs(
|
||||
int channelIndex,
|
||||
int timestampMs, {
|
||||
bool notify = true,
|
||||
}) {
|
||||
final existing = _channelLastReadMs[channelIndex] ?? 0;
|
||||
if (timestampMs <= existing) return;
|
||||
_channelLastReadMs[channelIndex] = timestampMs;
|
||||
_unreadStore.saveChannelLastRead(
|
||||
Map<int, int>.from(_channelLastReadMs),
|
||||
);
|
||||
_unreadStore.saveChannelLastRead(Map<int, int>.from(_channelLastReadMs));
|
||||
if (notify) {
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -2526,9 +2651,12 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
// Check if we've already processed this exact reaction using lightweight key
|
||||
_processedContactReactions.putIfAbsent(pubKeyHex, () => {});
|
||||
final reactionKey = reactionInfo.reactionKey;
|
||||
final reactionIdentifier = reactionKey != null ? '${reactionKey}_${reactionInfo.emoji}' : null;
|
||||
final reactionIdentifier = reactionKey != null
|
||||
? '${reactionKey}_${reactionInfo.emoji}'
|
||||
: null;
|
||||
|
||||
final isDuplicate = reactionIdentifier != null &&
|
||||
final isDuplicate =
|
||||
reactionIdentifier != null &&
|
||||
_processedContactReactions[pubKeyHex]!.contains(reactionIdentifier);
|
||||
|
||||
if (!isDuplicate) {
|
||||
@@ -2551,7 +2679,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _processContactReaction(List<Message> messages, ReactionInfo reactionInfo) {
|
||||
void _processContactReaction(
|
||||
List<Message> messages,
|
||||
ReactionInfo reactionInfo,
|
||||
) {
|
||||
// Find target message by messageId
|
||||
for (int i = 0; i < messages.length; i++) {
|
||||
if (messages[i].messageId == reactionInfo.targetMessageId) {
|
||||
@@ -2570,7 +2701,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
var index = 0;
|
||||
final header = raw[index++];
|
||||
final routeType = header & _phRouteMask;
|
||||
final hasTransport = routeType == _routeTransportFlood || routeType == _routeTransportDirect;
|
||||
final hasTransport =
|
||||
routeType == _routeTransportFlood || routeType == _routeTransportDirect;
|
||||
if (hasTransport) {
|
||||
if (raw.length < index + 4) return null;
|
||||
index += 4;
|
||||
@@ -2633,7 +2765,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
if (RegExp(r'[:\[\]]').hasMatch(potentialSender)) {
|
||||
return _ParsedText(senderName: 'Unknown', text: text);
|
||||
}
|
||||
final offset = (colonIndex + 1 < text.length && text[colonIndex + 1] == ' ')
|
||||
final offset =
|
||||
(colonIndex + 1 < text.length && text[colonIndex + 1] == ' ')
|
||||
? colonIndex + 2
|
||||
: colonIndex + 1;
|
||||
return _ParsedText(
|
||||
@@ -2670,10 +2803,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
return contact.path;
|
||||
}
|
||||
|
||||
int? _resolveOutgoingPathLength(
|
||||
Contact contact,
|
||||
PathSelection? selection,
|
||||
) {
|
||||
int? _resolveOutgoingPathLength(Contact contact, PathSelection? selection) {
|
||||
// Priority 1: Check user's path override
|
||||
if (contact.pathOverride != null) {
|
||||
return contact.pathOverride;
|
||||
@@ -2714,10 +2844,15 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
// Check if we've already processed this exact reaction using lightweight key
|
||||
_processedChannelReactions.putIfAbsent(channelIndex, () => {});
|
||||
final reactionKey = reactionInfo.reactionKey;
|
||||
final reactionIdentifier = reactionKey != null ? '${reactionKey}_${reactionInfo.emoji}' : null;
|
||||
final reactionIdentifier = reactionKey != null
|
||||
? '${reactionKey}_${reactionInfo.emoji}'
|
||||
: null;
|
||||
|
||||
final isDuplicate = reactionIdentifier != null &&
|
||||
_processedChannelReactions[channelIndex]!.contains(reactionIdentifier);
|
||||
final isDuplicate =
|
||||
reactionIdentifier != null &&
|
||||
_processedChannelReactions[channelIndex]!.contains(
|
||||
reactionIdentifier,
|
||||
);
|
||||
|
||||
if (!isDuplicate) {
|
||||
// New reaction - process it
|
||||
@@ -2739,7 +2874,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
if (replyInfo != null) {
|
||||
// Find original message by sender name (most recent match)
|
||||
final originalMessage = _findMessageBySender(messages, replyInfo.mentionedNode);
|
||||
final originalMessage = _findMessageBySender(
|
||||
messages,
|
||||
replyInfo.mentionedNode,
|
||||
);
|
||||
|
||||
if (originalMessage != null) {
|
||||
// Create new message with reply metadata
|
||||
@@ -2769,8 +2907,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
if (existingIndex >= 0) {
|
||||
isNew = false;
|
||||
final existing = messages[existingIndex];
|
||||
final mergedPathBytes = _selectPreferredPathBytes(existing.pathBytes, processedMessage.pathBytes);
|
||||
final mergedPathVariants = _mergePathVariants(existing.pathVariants, processedMessage.pathVariants);
|
||||
final mergedPathBytes = _selectPreferredPathBytes(
|
||||
existing.pathBytes,
|
||||
processedMessage.pathBytes,
|
||||
);
|
||||
final mergedPathVariants = _mergePathVariants(
|
||||
existing.pathVariants,
|
||||
processedMessage.pathVariants,
|
||||
);
|
||||
final mergedPathLength = _mergePathLength(
|
||||
existing.pathLength,
|
||||
processedMessage.pathLength,
|
||||
@@ -2783,7 +2927,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
pathBytes: mergedPathBytes,
|
||||
pathVariants: mergedPathVariants,
|
||||
// Mark as sent when first repeat is heard
|
||||
status: newRepeatCount == 1 && existing.status == ChannelMessageStatus.pending
|
||||
status:
|
||||
newRepeatCount == 1 &&
|
||||
existing.status == ChannelMessageStatus.pending
|
||||
? ChannelMessageStatus.sent
|
||||
: existing.status,
|
||||
);
|
||||
@@ -2792,14 +2938,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// Save to persistent storage
|
||||
_channelMessageStore.saveChannelMessages(
|
||||
channelIndex,
|
||||
messages,
|
||||
);
|
||||
_channelMessageStore.saveChannelMessages(channelIndex, messages);
|
||||
return isNew;
|
||||
}
|
||||
|
||||
ChannelMessage? _findMessageBySender(List<ChannelMessage> messages, String mentionedNode) {
|
||||
ChannelMessage? _findMessageBySender(
|
||||
List<ChannelMessage> messages,
|
||||
String mentionedNode,
|
||||
) {
|
||||
// Search backwards for most recent message from this sender
|
||||
for (int i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].senderName == mentionedNode && !messages[i].isOutgoing) {
|
||||
@@ -2809,7 +2955,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
return null;
|
||||
}
|
||||
|
||||
void _processReaction(List<ChannelMessage> messages, ReactionInfo reactionInfo) {
|
||||
void _processReaction(
|
||||
List<ChannelMessage> messages,
|
||||
ReactionInfo reactionInfo,
|
||||
) {
|
||||
// Find target message by messageId
|
||||
for (int i = 0; i < messages.length; i++) {
|
||||
if (messages[i].messageId == reactionInfo.targetMessageId) {
|
||||
@@ -2824,7 +2973,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
int _findChannelRepeatIndex(List<ChannelMessage> messages, ChannelMessage incoming) {
|
||||
int _findChannelRepeatIndex(
|
||||
List<ChannelMessage> messages,
|
||||
ChannelMessage incoming,
|
||||
) {
|
||||
for (int i = messages.length - 1; i >= 0; i--) {
|
||||
final existing = messages[i];
|
||||
if (_isChannelRepeat(existing, incoming)) {
|
||||
@@ -2837,7 +2989,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
bool _isChannelRepeat(ChannelMessage existing, ChannelMessage incoming) {
|
||||
if (existing.text != incoming.text) return false;
|
||||
|
||||
final diffMs = (existing.timestamp.millisecondsSinceEpoch -
|
||||
final diffMs =
|
||||
(existing.timestamp.millisecondsSinceEpoch -
|
||||
incoming.timestamp.millisecondsSinceEpoch)
|
||||
.abs();
|
||||
if (diffMs > 5000) return false;
|
||||
@@ -2960,6 +3113,34 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_scheduleReconnect();
|
||||
}
|
||||
|
||||
Map<String, String> _parseKeyValueString(String input) {
|
||||
final result = <String, String>{};
|
||||
|
||||
// Split on commas first – empty entries are ignored.
|
||||
for (final pair in input.split(',')) {
|
||||
final trimmedPair = pair.trim();
|
||||
if (trimmedPair.isEmpty) continue;
|
||||
|
||||
// Each pair must contain exactly one ':'.
|
||||
final separatorIndex = trimmedPair.indexOf(':');
|
||||
if (separatorIndex == -1) continue; // malformed, skip
|
||||
|
||||
final key = trimmedPair.substring(0, separatorIndex).trim();
|
||||
final value = trimmedPair.substring(separatorIndex + 1).trim();
|
||||
|
||||
if (key.isNotEmpty) {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void _handleCustomVars(Uint8List frame) {
|
||||
final buf = BufferReader(frame.sublist(1));
|
||||
_currentCustomVars = _parseKeyValueString(buf.readString());
|
||||
}
|
||||
|
||||
void _setState(MeshCoreConnectionState newState) {
|
||||
if (_state != newState) {
|
||||
_state = newState;
|
||||
@@ -3013,17 +3194,15 @@ class _RawPacket {
|
||||
required this.payload,
|
||||
});
|
||||
|
||||
bool get isFlood => routeType == _routeFlood || routeType == _routeTransportFlood;
|
||||
bool get isFlood =>
|
||||
routeType == _routeFlood || routeType == _routeTransportFlood;
|
||||
}
|
||||
|
||||
class _ParsedText {
|
||||
final String senderName;
|
||||
final String text;
|
||||
|
||||
_ParsedText({
|
||||
required this.senderName,
|
||||
required this.text,
|
||||
});
|
||||
_ParsedText({required this.senderName, required this.text});
|
||||
}
|
||||
|
||||
class _RepeaterAckContext {
|
||||
|
||||
@@ -20,7 +20,8 @@ class BufferReader {
|
||||
|
||||
Uint8List readRemainingBytes() => readBytes(remaining);
|
||||
|
||||
String readString() => utf8.decode(readRemainingBytes(), allowMalformed: true);
|
||||
String readString() =>
|
||||
utf8.decode(readRemainingBytes(), allowMalformed: true);
|
||||
|
||||
String readCString(int maxLength) {
|
||||
final value = <int>[];
|
||||
@@ -38,13 +39,19 @@ class BufferReader {
|
||||
|
||||
int readUInt8() => readBytes(1).buffer.asByteData().getUint8(0);
|
||||
int readInt8() => readBytes(1).buffer.asByteData().getInt8(0);
|
||||
int readUInt16LE() => readBytes(2).buffer.asByteData().getUint16(0, Endian.little);
|
||||
int readUInt16BE() => readBytes(2).buffer.asByteData().getUint16(0, Endian.big);
|
||||
int readUInt32LE() => readBytes(4).buffer.asByteData().getUint32(0, Endian.little);
|
||||
int readUInt32BE() => readBytes(4).buffer.asByteData().getUint32(0, Endian.big);
|
||||
int readInt16LE() => readBytes(2).buffer.asByteData().getInt16(0, Endian.little);
|
||||
int readUInt16LE() =>
|
||||
readBytes(2).buffer.asByteData().getUint16(0, Endian.little);
|
||||
int readUInt16BE() =>
|
||||
readBytes(2).buffer.asByteData().getUint16(0, Endian.big);
|
||||
int readUInt32LE() =>
|
||||
readBytes(4).buffer.asByteData().getUint32(0, Endian.little);
|
||||
int readUInt32BE() =>
|
||||
readBytes(4).buffer.asByteData().getUint32(0, Endian.big);
|
||||
int readInt16LE() =>
|
||||
readBytes(2).buffer.asByteData().getInt16(0, Endian.little);
|
||||
int readInt16BE() => readBytes(2).buffer.asByteData().getInt16(0, Endian.big);
|
||||
int readInt32LE() => readBytes(4).buffer.asByteData().getInt32(0, Endian.little);
|
||||
int readInt32LE() =>
|
||||
readBytes(4).buffer.asByteData().getInt32(0, Endian.little);
|
||||
|
||||
int readInt24BE() {
|
||||
var value = (readByte() << 16) | (readByte() << 8) | readByte();
|
||||
@@ -63,21 +70,25 @@ class BufferWriter {
|
||||
void writeBytes(Uint8List bytes) => _builder.add(bytes);
|
||||
|
||||
void writeUInt16LE(int num) {
|
||||
final bytes = Uint8List(2)..buffer.asByteData().setUint16(0, num, Endian.little);
|
||||
final bytes = Uint8List(2)
|
||||
..buffer.asByteData().setUint16(0, num, Endian.little);
|
||||
writeBytes(bytes);
|
||||
}
|
||||
|
||||
void writeUInt32LE(int num) {
|
||||
final bytes = Uint8List(4)..buffer.asByteData().setUint32(0, num, Endian.little);
|
||||
final bytes = Uint8List(4)
|
||||
..buffer.asByteData().setUint32(0, num, Endian.little);
|
||||
writeBytes(bytes);
|
||||
}
|
||||
|
||||
void writeInt32LE(int num) {
|
||||
final bytes = Uint8List(4)..buffer.asByteData().setInt32(0, num, Endian.little);
|
||||
final bytes = Uint8List(4)
|
||||
..buffer.asByteData().setInt32(0, num, Endian.little);
|
||||
writeBytes(bytes);
|
||||
}
|
||||
|
||||
void writeString(String string) => writeBytes(Uint8List.fromList(utf8.encode(string)));
|
||||
void writeString(String string) =>
|
||||
writeBytes(Uint8List.fromList(utf8.encode(string)));
|
||||
|
||||
void writeCString(String string, int maxLength) {
|
||||
final bytes = Uint8List(maxLength);
|
||||
@@ -118,6 +129,8 @@ const int cmdGetChannel = 31;
|
||||
const int cmdSetChannel = 32;
|
||||
const int cmdGetRadioSettings = 57;
|
||||
const int cmdGetTelemetryReq = 39;
|
||||
const int cmdGetCustomVar = 40;
|
||||
const int cmdSetCustomVar = 41;
|
||||
const int cmdSendBinaryReq = 50;
|
||||
|
||||
// Text message types
|
||||
@@ -152,6 +165,7 @@ const int respCodeContactMsgRecvV3 = 16;
|
||||
const int respCodeChannelMsgRecvV3 = 17;
|
||||
const int respCodeChannelInfo = 18;
|
||||
const int respCodeRadioSettings = 25;
|
||||
const int respCodeCustomVars = 21;
|
||||
|
||||
// Push codes (async from device)
|
||||
const int pushCodeAdvert = 0x80;
|
||||
@@ -166,7 +180,6 @@ const int pushCodeNewAdvert = 0x8A;
|
||||
const int pushCodeTelemetryResponse = 0x8B;
|
||||
const int pushCodeBinaryResponse = 0x8C;
|
||||
|
||||
|
||||
// Contact/advertisement types
|
||||
const int advTypeChat = 1;
|
||||
const int advTypeRepeater = 2;
|
||||
@@ -233,10 +246,7 @@ class ParsedContactText {
|
||||
final Uint8List senderPrefix;
|
||||
final String text;
|
||||
|
||||
const ParsedContactText({
|
||||
required this.senderPrefix,
|
||||
required this.text,
|
||||
});
|
||||
const ParsedContactText({required this.senderPrefix, required this.text});
|
||||
}
|
||||
|
||||
ParsedContactText? parseContactMessageText(Uint8List frame) {
|
||||
@@ -265,10 +275,17 @@ ParsedContactText? parseContactMessageText(Uint8List frame) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var text = readCString(frame, baseTextOffset, frame.length - baseTextOffset).trim();
|
||||
var text = readCString(
|
||||
frame,
|
||||
baseTextOffset,
|
||||
frame.length - baseTextOffset,
|
||||
).trim();
|
||||
if (text.isEmpty && frame.length > baseTextOffset + 4) {
|
||||
text =
|
||||
readCString(frame, baseTextOffset + 4, frame.length - (baseTextOffset + 4)).trim();
|
||||
text = readCString(
|
||||
frame,
|
||||
baseTextOffset + 4,
|
||||
frame.length - (baseTextOffset + 4),
|
||||
).trim();
|
||||
}
|
||||
if (text.isEmpty) return null;
|
||||
|
||||
@@ -362,7 +379,8 @@ Uint8List buildSendTextMsgFrame(
|
||||
int attempt = 0,
|
||||
int? timestampSeconds,
|
||||
}) {
|
||||
final timestamp = timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
|
||||
final timestamp =
|
||||
timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdSendTxtMsg);
|
||||
writer.writeByte(txtTypePlain);
|
||||
@@ -444,7 +462,9 @@ Uint8List buildSendSelfAdvertFrame({bool flood = false}) {
|
||||
// Format: [cmd][name...]
|
||||
Uint8List buildSetAdvertNameFrame(String name) {
|
||||
final nameBytes = utf8.encode(name);
|
||||
final nameLen = nameBytes.length < maxNameSize ? nameBytes.length : maxNameSize - 1;
|
||||
final nameLen = nameBytes.length < maxNameSize
|
||||
? nameBytes.length
|
||||
: maxNameSize - 1;
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdSetAdvertName);
|
||||
writer.writeBytes(Uint8List.fromList(nameBytes.sublist(0, nameLen)));
|
||||
@@ -461,6 +481,14 @@ Uint8List buildSetAdvertLatLonFrame(double lat, double lon) {
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
Uint8List buildSetCustomVarFrame(String value) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdSetCustomVar);
|
||||
writer.writeString(value);
|
||||
writer.writeByte(0);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
// Build CMD_REBOOT frame
|
||||
// Format: [cmd]["reboot"]
|
||||
Uint8List buildRebootFrame() {
|
||||
@@ -544,7 +572,9 @@ Uint8List buildUpdateContactPathFrame(
|
||||
// Path data (64 bytes, zero-padded)
|
||||
final pathPadded = Uint8List(maxPathSize);
|
||||
if (customPath.isNotEmpty && pathLen > 0) {
|
||||
final copyLen = customPath.length < maxPathSize ? customPath.length : maxPathSize;
|
||||
final copyLen = customPath.length < maxPathSize
|
||||
? customPath.length
|
||||
: maxPathSize;
|
||||
for (int i = 0; i < copyLen; i++) {
|
||||
pathPadded[i] = customPath[i];
|
||||
}
|
||||
@@ -575,6 +605,11 @@ Uint8List buildGetRadioSettingsFrame() {
|
||||
return Uint8List.fromList([cmdGetRadioSettings]);
|
||||
}
|
||||
|
||||
//Build CMD_GET_CUSTOM_VARS frame
|
||||
Uint8List buildGetCustomVarsFrame() {
|
||||
return Uint8List.fromList([cmdGetCustomVar]);
|
||||
}
|
||||
|
||||
// Calculate LoRa airtime for a packet
|
||||
// Based on Semtech SX127x datasheet formula
|
||||
// Returns airtime in milliseconds
|
||||
@@ -598,9 +633,11 @@ int calculateLoRaAirtime({
|
||||
final crc = 1; // CRC enabled
|
||||
final de = lowDataRateOptimize ? 1 : 0;
|
||||
|
||||
final numerator = 8 * payloadBytes - 4 * spreadingFactor + 28 + 16 * crc - headerBytes;
|
||||
final numerator =
|
||||
8 * payloadBytes - 4 * spreadingFactor + 28 + 16 * crc - headerBytes;
|
||||
final denominator = 4 * (spreadingFactor - 2 * de);
|
||||
var payloadSymbols = 8 + ((numerator / denominator).ceil()) * (codingRate + 4);
|
||||
var payloadSymbols =
|
||||
8 + ((numerator / denominator).ceil()) * (codingRate + 4);
|
||||
|
||||
if (payloadSymbols < 0) {
|
||||
payloadSymbols = 8;
|
||||
@@ -647,7 +684,8 @@ Uint8List buildSendCliCommandFrame(
|
||||
int attempt = 0,
|
||||
int? timestampSeconds,
|
||||
}) {
|
||||
final timestamp = timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
|
||||
final timestamp =
|
||||
timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdSendTxtMsg);
|
||||
writer.writeByte(txtTypeCliData);
|
||||
@@ -661,10 +699,7 @@ Uint8List buildSendCliCommandFrame(
|
||||
|
||||
// Build a telemetry request frame
|
||||
// Format: [cmd][pub_key x32][payload]
|
||||
Uint8List buildSendBinaryReq(
|
||||
Uint8List repeaterPubKey, {
|
||||
Uint8List? payload,
|
||||
}) {
|
||||
Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdSendBinaryReq);
|
||||
writer.writeBytes(repeaterPubKey);
|
||||
|
||||
@@ -1349,6 +1349,10 @@
|
||||
"channels_scanQrCodeComingSoon": "Ще излезе скоро",
|
||||
"channels_enterHashtag": "Въведете хаштаг",
|
||||
"channels_hashtagHint": "напр. #отбор",
|
||||
"settings_locationIntervalSec": "Интервал за GPS (Секунди)",
|
||||
"settings_locationGPSEnable": "Активиране на GPS",
|
||||
"settings_locationGPSEnableSubtitle": "Активирайте автоматичното актуализиране на местоположението чрез GPS.",
|
||||
"settings_locationIntervalInvalid": "Интервалът трябва да бъде поне 60 секунди и по-малко от 86400 секунди.",
|
||||
"room_management": "Управление на сървъра за стая",
|
||||
"contacts_manageRoom": "Управление на сървър за стая"
|
||||
}
|
||||
|
||||
+1
-3
@@ -1348,7 +1348,5 @@
|
||||
"channels_scanQrCode": "Scannen Sie einen QR-Code",
|
||||
"channels_scanQrCodeComingSoon": "Bald verfügbar",
|
||||
"channels_enterHashtag": "Gib Hashtag ein",
|
||||
"channels_hashtagHint": "z.B. #team",
|
||||
"contacts_manageRoom": "Verwalten Sie den Raumserver",
|
||||
"room_management": "Raumserververwaltung"
|
||||
"channels_hashtagHint": "z.B. #team"
|
||||
}
|
||||
|
||||
+5
-1
@@ -83,9 +83,13 @@
|
||||
"settings_radioSettingsUpdated": "Radio settings updated",
|
||||
"settings_location": "Location",
|
||||
"settings_locationSubtitle": "GPS coordinates",
|
||||
"settings_locationUpdated": "Location updated",
|
||||
"settings_locationUpdated": "Location and GPS settings updated",
|
||||
"settings_locationBothRequired": "Enter both latitude and longitude.",
|
||||
"settings_locationInvalid": "Invalid latitude or longitude.",
|
||||
"settings_locationGPSEnable": "GPS Enable",
|
||||
"settings_locationGPSEnableSubtitle": "Enables GPS to automatically update location.",
|
||||
"settings_locationIntervalSec": "Interval for GPS (Seconds)",
|
||||
"settings_locationIntervalInvalid": "Interval must be at least 60 seconds, and less than 86400 seconds.",
|
||||
"settings_latitude": "Latitude",
|
||||
"settings_longitude": "Longitude",
|
||||
"settings_privacyMode": "Privacy Mode",
|
||||
|
||||
@@ -1349,6 +1349,10 @@
|
||||
"channels_scanQrCodeComingSoon": "Próximamente",
|
||||
"channels_enterHashtag": "Introducir hashtag",
|
||||
"channels_hashtagHint": "ej. #equipo",
|
||||
"settings_locationGPSEnable": "Habilitar GPS",
|
||||
"settings_locationGPSEnableSubtitle": "Habilita la actualización automática de la ubicación mediante GPS.",
|
||||
"settings_locationIntervalSec": "Intervalo para GPS (Segundos)",
|
||||
"settings_locationIntervalInvalid": "El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.",
|
||||
"contacts_manageRoom": "Gestionar Servidor de Habitación",
|
||||
"room_management": "Administración del Servidor de Habitación"
|
||||
}
|
||||
|
||||
+6
-2
@@ -1349,6 +1349,10 @@
|
||||
"channels_scanQrCodeComingSoon": "Bientôt disponible",
|
||||
"channels_enterHashtag": "Entrez le hashtag",
|
||||
"channels_hashtagHint": "ex. #équipe",
|
||||
"contacts_manageRoom": "Gérer le serveur de salle",
|
||||
"room_management": "Gestion du serveur de pièce"
|
||||
"settings_locationGPSEnable": "Habilita GPS",
|
||||
"settings_locationGPSEnableSubtitle": "Habilita la actualización automática de la ubicación mediante GPS.",
|
||||
"settings_locationIntervalSec": "Intervalo pour GPS (Segundos)",
|
||||
"settings_locationIntervalInvalid": "El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.",
|
||||
"contacts_manageRoom": "Gestionar Servidor de Habitación",
|
||||
"room_management": "Administración del Servidor de Habitación"
|
||||
}
|
||||
|
||||
+6
-2
@@ -1349,6 +1349,10 @@
|
||||
"channels_scanQrCodeComingSoon": "Arriverà presto",
|
||||
"channels_enterHashtag": "Inserisci hashtag",
|
||||
"channels_hashtagHint": "es. #team",
|
||||
"room_management": "Gestione del Server di Camera",
|
||||
"contacts_manageRoom": "Gestisci Server Camera"
|
||||
"settings_locationGPSEnable": "Abilita GPS",
|
||||
"settings_locationGPSEnableSubtitle": "Abilita l'aggiornamento automatico della posizione tramite GPS.",
|
||||
"settings_locationIntervalSec": "Intervallo GPS (Secondi)",
|
||||
"settings_locationIntervalInvalid": "L'intervallo deve essere di almeno 60 secondi e inferiore a 86400 secondi.",
|
||||
"contacts_manageRoom": "Gestisci Server Camera",
|
||||
"room_management": "Gestione del Server di Camera"
|
||||
}
|
||||
|
||||
@@ -465,7 +465,7 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @settings_locationUpdated.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Location updated'**
|
||||
/// **'Location and GPS settings updated'**
|
||||
String get settings_locationUpdated;
|
||||
|
||||
/// No description provided for @settings_locationBothRequired.
|
||||
@@ -480,6 +480,30 @@ abstract class AppLocalizations {
|
||||
/// **'Invalid latitude or longitude.'**
|
||||
String get settings_locationInvalid;
|
||||
|
||||
/// No description provided for @settings_locationGPSEnable.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'GPS Enable'**
|
||||
String get settings_locationGPSEnable;
|
||||
|
||||
/// No description provided for @settings_locationGPSEnableSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enables GPS to automatically update location.'**
|
||||
String get settings_locationGPSEnableSubtitle;
|
||||
|
||||
/// No description provided for @settings_locationIntervalSec.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Interval for GPS (Seconds)'**
|
||||
String get settings_locationIntervalSec;
|
||||
|
||||
/// No description provided for @settings_locationIntervalInvalid.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Interval must be at least 60 seconds, and less than 86400 seconds.'**
|
||||
String get settings_locationIntervalInvalid;
|
||||
|
||||
/// No description provided for @settings_latitude.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -201,6 +201,20 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get settings_locationInvalid => 'Невалидна ширина или дължина.';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnable => 'Активиране на GPS';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnableSubtitle =>
|
||||
'Активирайте автоматичното актуализиране на местоположението чрез GPS.';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalSec => 'Интервал за GPS (Секунди)';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalInvalid =>
|
||||
'Интервалът трябва да бъде поне 60 секунди и по-малко от 86400 секунди.';
|
||||
|
||||
@override
|
||||
String get settings_latitude => 'Широчина';
|
||||
|
||||
|
||||
@@ -200,6 +200,20 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get settings_locationInvalid => 'Ungültige Breiten- oder Längengrade.';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnable => 'GPS Enable';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnableSubtitle =>
|
||||
'Enables GPS to automatically update location.';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalSec => 'Interval for GPS (Seconds)';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalInvalid =>
|
||||
'Interval must be at least 60 seconds, and less than 86400 seconds.';
|
||||
|
||||
@override
|
||||
String get settings_latitude => 'Breitengrad';
|
||||
|
||||
@@ -648,7 +662,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get contacts_manageRepeater => 'Wiederholungen verwalten';
|
||||
|
||||
@override
|
||||
String get contacts_manageRoom => 'Verwalten Sie den Raumserver';
|
||||
String get contacts_manageRoom => 'Manage Room Server';
|
||||
|
||||
@override
|
||||
String get contacts_roomLogin => 'Raum-Login';
|
||||
@@ -1590,7 +1604,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get repeater_management => 'Repeater-Verwaltung';
|
||||
|
||||
@override
|
||||
String get room_management => 'Raumserververwaltung';
|
||||
String get room_management => 'Room Server Management';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Verwaltungs-Tools';
|
||||
|
||||
@@ -190,7 +190,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get settings_locationSubtitle => 'GPS coordinates';
|
||||
|
||||
@override
|
||||
String get settings_locationUpdated => 'Location updated';
|
||||
String get settings_locationUpdated => 'Location and GPS settings updated';
|
||||
|
||||
@override
|
||||
String get settings_locationBothRequired =>
|
||||
@@ -199,6 +199,20 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get settings_locationInvalid => 'Invalid latitude or longitude.';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnable => 'GPS Enable';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnableSubtitle =>
|
||||
'Enables GPS to automatically update location.';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalSec => 'Interval for GPS (Seconds)';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalInvalid =>
|
||||
'Interval must be at least 60 seconds, and less than 86400 seconds.';
|
||||
|
||||
@override
|
||||
String get settings_latitude => 'Latitude';
|
||||
|
||||
|
||||
@@ -200,6 +200,20 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get settings_locationInvalid => 'Latitud o longitud inválidos.';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnable => 'Habilitar GPS';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnableSubtitle =>
|
||||
'Habilita la actualización automática de la ubicación mediante GPS.';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalSec => 'Intervalo para GPS (Segundos)';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalInvalid =>
|
||||
'El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.';
|
||||
|
||||
@override
|
||||
String get settings_latitude => 'Latitud';
|
||||
|
||||
|
||||
@@ -200,6 +200,20 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get settings_locationInvalid => 'Latitude ou longitude invalide.';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnable => 'Habilita GPS';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnableSubtitle =>
|
||||
'Habilita la actualización automática de la ubicación mediante GPS.';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalSec => 'Intervalo pour GPS (Segundos)';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalInvalid =>
|
||||
'El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.';
|
||||
|
||||
@override
|
||||
String get settings_latitude => 'Latitude';
|
||||
|
||||
@@ -650,7 +664,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get contacts_manageRepeater => 'Gérer le répétiteur';
|
||||
|
||||
@override
|
||||
String get contacts_manageRoom => 'Gérer le serveur de salle';
|
||||
String get contacts_manageRoom => 'Gestionar Servidor de Habitación';
|
||||
|
||||
@override
|
||||
String get contacts_roomLogin => 'Connexion Salle';
|
||||
@@ -1595,7 +1609,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get repeater_management => 'Gestion des répétiteurs';
|
||||
|
||||
@override
|
||||
String get room_management => 'Gestion du serveur de pièce';
|
||||
String get room_management => 'Administración del Servidor de Habitación';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Outils de Gestion';
|
||||
|
||||
@@ -200,6 +200,20 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get settings_locationInvalid => 'Latitudine o longitudine non valida.';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnable => 'Abilita GPS';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnableSubtitle =>
|
||||
'Abilita l\'aggiornamento automatico della posizione tramite GPS.';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalSec => 'Intervallo GPS (Secondi)';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalInvalid =>
|
||||
'L\'intervallo deve essere di almeno 60 secondi e inferiore a 86400 secondi.';
|
||||
|
||||
@override
|
||||
String get settings_latitude => 'Latitudine';
|
||||
|
||||
|
||||
@@ -200,6 +200,20 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get settings_locationInvalid =>
|
||||
'Ongeldige breedtegraad of lengtegraad.';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnable => 'GPS inschakelen';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnableSubtitle =>
|
||||
'Activeer automatisch locatieupdates via GPS.';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalSec => 'Interval voor GPS (Seconden)';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalInvalid =>
|
||||
'De intervallen moeten minstens 60 seconden zijn en minder dan 86400 seconden.';
|
||||
|
||||
@override
|
||||
String get settings_latitude => 'Breedtegraad';
|
||||
|
||||
|
||||
@@ -202,6 +202,20 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String get settings_locationInvalid =>
|
||||
'Nieprawidłowa szerokość geograficzna lub długość geograficzna.';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnable => 'Włącz GPS';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnableSubtitle =>
|
||||
'Włącza automatyczne aktualizowanie pozycji za pomocą GPS.';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalSec => 'Interwał dla GPS (Sekundy)';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalInvalid =>
|
||||
'Interwał musi wynosić co najmniej 60 sekund i mniej niż 86400 sekund.';
|
||||
|
||||
@override
|
||||
String get settings_latitude => 'Szerokość';
|
||||
|
||||
|
||||
@@ -201,6 +201,20 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get settings_locationInvalid => 'Latitude ou longitude inválidos.';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnable => 'Ativar GPS';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnableSubtitle =>
|
||||
'Habilita a atualização automática da localização via GPS.';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalSec => 'Intervalo para GPS (Segundos)';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalInvalid =>
|
||||
'O intervalo deve ser de pelo menos 60 segundos e inferior a 86400 segundos.';
|
||||
|
||||
@override
|
||||
String get settings_latitude => 'Latitude';
|
||||
|
||||
|
||||
@@ -200,6 +200,20 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get settings_locationInvalid => 'Neplatná šírka alebo dĺžka.';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnable => 'Aktivovať GPS';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnableSubtitle =>
|
||||
'Povolí automatické aktualizovanie polohy pomocou GPS.';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalSec => 'Interval pre GPS (Sekundy)';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalInvalid =>
|
||||
'Interval musí byť aspoň 60 sekúnd a menej ako 86400 sekúnd.';
|
||||
|
||||
@override
|
||||
String get settings_latitude => 'Súradnica';
|
||||
|
||||
@@ -1103,7 +1117,7 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get chat_clearPathSubtitle =>
|
||||
'Znovu nájsť vynútene pri nasledujacej pošlite';
|
||||
'Znovu nájsť vynútene pri nasledujúcej pošlite';
|
||||
|
||||
@override
|
||||
String get chat_pathCleared =>
|
||||
|
||||
@@ -200,6 +200,20 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String get settings_locationInvalid =>
|
||||
'Neveljna zemeljska širina ali dolžina.';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnable => 'Omogoči GPS';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnableSubtitle =>
|
||||
'Omogoči samodejno posodabljanje lokacije z GPS-jem.';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalSec => 'Interval za GPS (Sekunde)';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalInvalid =>
|
||||
'Intervallo mora biti vsaj 60 sekund in manj kot 86400 sekund.';
|
||||
|
||||
@override
|
||||
String get settings_latitude => 'Širina';
|
||||
|
||||
@@ -435,7 +449,7 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get appSettings_enableNotificationsSubtitle =>
|
||||
'Prejmujte obvestila o sporočilih in oglasih';
|
||||
'Prejmite obvestila o sporočilih in oglasih';
|
||||
|
||||
@override
|
||||
String get appSettings_notificationPermissionDenied =>
|
||||
@@ -631,7 +645,7 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contacts_noContactsFound =>
|
||||
'Niti ena osebe ali skupine ni najdena.';
|
||||
'Niti ena oseba ali skupine ni najdena.';
|
||||
|
||||
@override
|
||||
String get contacts_deleteContact => 'Izbrisati Kontakt';
|
||||
@@ -683,7 +697,7 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get contacts_noContactsMatchFilter =>
|
||||
'Niti ena osebe ne ustreza vašemu kriteriju.';
|
||||
'Niti ena oseba ne ustreza vašemu kriteriju.';
|
||||
|
||||
@override
|
||||
String get contacts_noMembers => 'Nič članov.';
|
||||
@@ -1189,7 +1203,7 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get map_nodesNeedGps =>
|
||||
'Omrežje morajo deliti svoje GPS koordinate,\nda se prikazajo na zemljeobrazniku.';
|
||||
'Omrežje morajo deliti svoje GPS koordinate,\nda se prikazao na zemljeobrazniku.';
|
||||
|
||||
@override
|
||||
String map_nodesCount(int count) {
|
||||
|
||||
@@ -199,6 +199,20 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get settings_locationInvalid => 'Ogiltig latitud eller longitud.';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnable => 'Aktivera GPS';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnableSubtitle =>
|
||||
'Aktivera automatiska uppdateringar av platsen med hjälp av GPS.';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalSec => 'Interval för GPS (Sekunder)';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalInvalid =>
|
||||
'Intervalet måste vara minst 60 sekunder och mindre än 86400 sekunder.';
|
||||
|
||||
@override
|
||||
String get settings_latitude => 'Latitud';
|
||||
|
||||
|
||||
@@ -196,6 +196,18 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get settings_locationInvalid => '无效的纬度或经度。';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnable => '启用GPS';
|
||||
|
||||
@override
|
||||
String get settings_locationGPSEnableSubtitle => '启用GPS自动更新位置。';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalSec => 'GPS 间隔(秒)';
|
||||
|
||||
@override
|
||||
String get settings_locationIntervalInvalid => '时间间隔必须至少为60秒,且小于86400秒。';
|
||||
|
||||
@override
|
||||
String get settings_latitude => '纬度';
|
||||
|
||||
|
||||
+6
-2
@@ -1349,6 +1349,10 @@
|
||||
"channels_scanQrCodeComingSoon": "Komt later",
|
||||
"channels_enterHashtag": "Voer hashtag in",
|
||||
"channels_hashtagHint": "bijv. #team",
|
||||
"room_management": "Beheer Server Kamer",
|
||||
"contacts_manageRoom": "Beheer Ruimte Server"
|
||||
"settings_locationGPSEnable": "GPS inschakelen",
|
||||
"settings_locationGPSEnableSubtitle": "Activeer automatisch locatieupdates via GPS.",
|
||||
"settings_locationIntervalSec": "Interval voor GPS (Seconden)",
|
||||
"settings_locationIntervalInvalid": "De intervallen moeten minstens 60 seconden zijn en minder dan 86400 seconden.",
|
||||
"contacts_manageRoom": "Beheer Ruimte Server",
|
||||
"room_management": "Beheer Server Kamer"
|
||||
}
|
||||
|
||||
@@ -1349,6 +1349,10 @@
|
||||
"channels_scanQrCodeComingSoon": "Wkrótce",
|
||||
"channels_enterHashtag": "Wprowadź hashtag",
|
||||
"channels_hashtagHint": "np. #zespół",
|
||||
"settings_locationGPSEnable": "Włącz GPS",
|
||||
"settings_locationGPSEnableSubtitle": "Włącza automatyczne aktualizowanie pozycji za pomocą GPS.",
|
||||
"settings_locationIntervalSec": "Interwał dla GPS (Sekundy)",
|
||||
"settings_locationIntervalInvalid": "Interwał musi wynosić co najmniej 60 sekund i mniej niż 86400 sekund.",
|
||||
"contacts_manageRoom": "Zarządzaj Serwerem Pokoju",
|
||||
"room_management": "Zarządzanie Serwerem Pokoju"
|
||||
}
|
||||
|
||||
@@ -1349,6 +1349,10 @@
|
||||
"channels_scanQrCodeComingSoon": "Em breve",
|
||||
"channels_enterHashtag": "Insira hashtag",
|
||||
"channels_hashtagHint": "ex. #equipe",
|
||||
"settings_locationGPSEnable": "Ativar GPS",
|
||||
"settings_locationGPSEnableSubtitle": "Habilita a atualização automática da localização via GPS.",
|
||||
"settings_locationIntervalInvalid": "O intervalo deve ser de pelo menos 60 segundos e inferior a 86400 segundos.",
|
||||
"settings_locationIntervalSec": "Intervalo para GPS (Segundos)",
|
||||
"contacts_manageRoom": "Gerenciar Servidor de Sala",
|
||||
"room_management": "Gerenciamento de Servidor de Sala"
|
||||
}
|
||||
|
||||
+7
-3
@@ -559,7 +559,7 @@
|
||||
"chat_setCustomPath": "Nastaviť vlastnú cestu",
|
||||
"chat_setCustomPathSubtitle": "Ručne zadajte trasu.",
|
||||
"chat_clearPath": "Vyčistiš cestu",
|
||||
"chat_clearPathSubtitle": "Znovu nájsť vynútene pri nasledujacej pošlite",
|
||||
"chat_clearPathSubtitle": "Znovu nájsť vynútene pri nasledujúcej pošlite",
|
||||
"chat_pathCleared": "Cesta vyčistená. Nasledujúce prepočetné získa trasu znova.",
|
||||
"chat_floodModeSubtitle": "Použite prepínanie trasy v navigačnom paneli.",
|
||||
"chat_floodModeEnabled": "Odosporňovacia prevádzka je zapnutá. Vypnite ju znova cez ikonu routovania v navigačnom páse.",
|
||||
@@ -1349,6 +1349,10 @@
|
||||
"channels_scanQrCodeComingSoon": "Čoskoro",
|
||||
"channels_enterHashtag": "Zadajte hashtag",
|
||||
"channels_hashtagHint": "napr. #tím",
|
||||
"room_management": "Správa servera miestnosti",
|
||||
"contacts_manageRoom": "Spravovať server miestnosti"
|
||||
"settings_locationGPSEnable": "Aktivovať GPS",
|
||||
"settings_locationGPSEnableSubtitle": "Povolí automatické aktualizovanie polohy pomocou GPS.",
|
||||
"settings_locationIntervalSec": "Interval pre GPS (Sekundy)",
|
||||
"settings_locationIntervalInvalid": "Interval musí byť aspoň 60 sekúnd a menej ako 86400 sekúnd.",
|
||||
"contacts_manageRoom": "Spravovať server miestnosti",
|
||||
"room_management": "Správa servera miestnosti"
|
||||
}
|
||||
|
||||
+8
-4
@@ -176,7 +176,7 @@
|
||||
"appSettings_languageBg": "Български",
|
||||
"appSettings_notifications": "Obveščanja",
|
||||
"appSettings_enableNotifications": "Omogoči obveščanje",
|
||||
"appSettings_enableNotificationsSubtitle": "Prejmujte obvestila o sporočilih in oglasih",
|
||||
"appSettings_enableNotificationsSubtitle": "Prejmite obvestila o sporočilih in oglasih",
|
||||
"appSettings_notificationPermissionDenied": "Odobritev obvestila zavrnjena",
|
||||
"appSettings_notificationsEnabled": "Obvestila omogočena",
|
||||
"appSettings_notificationsDisabled": "Obvestila so izklopljena",
|
||||
@@ -256,7 +256,7 @@
|
||||
"contacts_contactsWillAppear": "Kontakti se bodo prikazali, ko naprave oglasijo.",
|
||||
"contacts_searchContacts": "Iskanje kontaktov...",
|
||||
"contacts_noUnreadContacts": "Nerešeno kontaktov.",
|
||||
"contacts_noContactsFound": "Niti ena osebe ali skupine ni najdena.",
|
||||
"contacts_noContactsFound": "Niti ena oseba ali skupine ni najdena.",
|
||||
"contacts_deleteContact": "Izbrisati Kontakt",
|
||||
"contacts_removeConfirm": "Izbrisati {contactName} iz kontaktov?",
|
||||
"@contacts_removeConfirm": {
|
||||
@@ -291,7 +291,7 @@
|
||||
}
|
||||
},
|
||||
"contacts_filterContacts": "Filtri kontakt\\,...",
|
||||
"contacts_noContactsMatchFilter": "Niti ena osebe ne ustreza vašemu kriteriju.",
|
||||
"contacts_noContactsMatchFilter": "Niti ena oseba ne ustreza vašemu kriteriju.",
|
||||
"contacts_noMembers": "Nič članov.",
|
||||
"contacts_lastSeenNow": "Datum zadnjega vpisa zdaj",
|
||||
"contacts_lastSeenMinsAgo": "Zadnjič videti {minutes} minut nazaj",
|
||||
@@ -606,7 +606,7 @@
|
||||
},
|
||||
"map_title": "Mapa omrežja",
|
||||
"map_noNodesWithLocation": "Nihče od notranjih elementov nima podatkov o lokaciji.",
|
||||
"map_nodesNeedGps": "Omrežje morajo deliti svoje GPS koordinate,\nda se prikazajo na zemljeobrazniku.",
|
||||
"map_nodesNeedGps": "Omrežje morajo deliti svoje GPS koordinate,\nda se prikazao na zemljeobrazniku.",
|
||||
"map_nodesCount": "Omize: {count}",
|
||||
"@map_nodesCount": {
|
||||
"placeholders": {
|
||||
@@ -1349,6 +1349,10 @@
|
||||
"channels_scanQrCodeComingSoon": "Prihajajoča",
|
||||
"channels_enterHashtag": "Vnesite hashtag",
|
||||
"channels_hashtagHint": "npr. #ekipa",
|
||||
"settings_locationGPSEnable": "Omogoči GPS",
|
||||
"settings_locationGPSEnableSubtitle": "Omogoči samodejno posodabljanje lokacije z GPS-jem.",
|
||||
"settings_locationIntervalSec": "Interval za GPS (Sekunde)",
|
||||
"settings_locationIntervalInvalid": "Intervallo mora biti vsaj 60 sekund in manj kot 86400 sekund.",
|
||||
"contacts_manageRoom": "Upravljajte strežnik sobe",
|
||||
"room_management": "Upravljanje stremlišča"
|
||||
}
|
||||
|
||||
@@ -1349,6 +1349,10 @@
|
||||
"channels_scanQrCodeComingSoon": "Kommer snart",
|
||||
"channels_enterHashtag": "Ange hashtag",
|
||||
"channels_hashtagHint": "t.ex. #team",
|
||||
"settings_locationGPSEnable": "Aktivera GPS",
|
||||
"settings_locationGPSEnableSubtitle": "Aktivera automatiska uppdateringar av platsen med hjälp av GPS.",
|
||||
"settings_locationIntervalSec": "Interval för GPS (Sekunder)",
|
||||
"settings_locationIntervalInvalid": "Intervalet måste vara minst 60 sekunder och mindre än 86400 sekunder.",
|
||||
"contacts_manageRoom": "Hantera Rumserver",
|
||||
"room_management": "Rumserverhantering"
|
||||
}
|
||||
|
||||
@@ -1349,6 +1349,10 @@
|
||||
"channels_scanQrCodeComingSoon": "即将到来",
|
||||
"channels_enterHashtag": "输入标签",
|
||||
"channels_hashtagHint": "例如 #团队",
|
||||
"settings_locationGPSEnable": "启用GPS",
|
||||
"settings_locationGPSEnableSubtitle": "启用GPS自动更新位置。",
|
||||
"settings_locationIntervalSec": "GPS 间隔(秒)",
|
||||
"settings_locationIntervalInvalid": "时间间隔必须至少为60秒,且小于86400秒。",
|
||||
"contacts_manageRoom": "管理房间服务器",
|
||||
"room_management": "房间服务器管理"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:meshcore_open/widgets/elements_ui.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
@@ -38,10 +39,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.settings_title),
|
||||
centerTitle: true,
|
||||
),
|
||||
appBar: AppBar(title: Text(l10n.settings_title), centerTitle: true),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Consumer<MeshCoreConnector>(
|
||||
@@ -68,7 +66,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDeviceInfoCard(BuildContext context, MeshCoreConnector connector) {
|
||||
Widget _buildDeviceInfoCard(
|
||||
BuildContext context,
|
||||
MeshCoreConnector connector,
|
||||
) {
|
||||
final l10n = context.l10n;
|
||||
return Card(
|
||||
child: Padding(
|
||||
@@ -83,21 +84,38 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoRow(l10n.settings_infoName, connector.deviceDisplayName),
|
||||
_buildInfoRow(l10n.settings_infoId, connector.deviceIdLabel),
|
||||
_buildInfoRow(l10n.settings_infoStatus, connector.isConnected ? l10n.common_connected : l10n.common_disconnected),
|
||||
_buildInfoRow(
|
||||
l10n.settings_infoStatus,
|
||||
connector.isConnected
|
||||
? l10n.common_connected
|
||||
: l10n.common_disconnected,
|
||||
),
|
||||
_buildBatteryInfoRow(context, connector),
|
||||
if (connector.selfName != null)
|
||||
_buildInfoRow(l10n.settings_nodeName, connector.selfName!),
|
||||
if (connector.selfPublicKey != null)
|
||||
_buildInfoRow(l10n.settings_infoPublicKey, '${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...'),
|
||||
_buildInfoRow(l10n.settings_infoContactsCount, '${connector.contacts.length}'),
|
||||
_buildInfoRow(l10n.settings_infoChannelCount, '${connector.channels.length}'),
|
||||
_buildInfoRow(
|
||||
l10n.settings_infoPublicKey,
|
||||
'${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...',
|
||||
),
|
||||
_buildInfoRow(
|
||||
l10n.settings_infoContactsCount,
|
||||
'${connector.contacts.length}',
|
||||
),
|
||||
_buildInfoRow(
|
||||
l10n.settings_infoChannelCount,
|
||||
'${connector.channels.length}',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBatteryInfoRow(BuildContext context, MeshCoreConnector connector) {
|
||||
Widget _buildBatteryInfoRow(
|
||||
BuildContext context,
|
||||
MeshCoreConnector connector,
|
||||
) {
|
||||
final l10n = context.l10n;
|
||||
final percent = connector.batteryPercent;
|
||||
final millivolts = connector.batteryMillivolts;
|
||||
@@ -167,7 +185,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNodeSettingsCard(BuildContext context, MeshCoreConnector connector) {
|
||||
Widget _buildNodeSettingsCard(
|
||||
BuildContext context,
|
||||
MeshCoreConnector connector,
|
||||
) {
|
||||
final l10n = context.l10n;
|
||||
return Card(
|
||||
child: Column(
|
||||
@@ -298,7 +319,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const BleDebugLogScreen()),
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const BleDebugLogScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -311,7 +334,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const AppDebugLogScreen()),
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const AppDebugLogScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -334,20 +359,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (leading != null) ...[
|
||||
leading,
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
if (leading != null) ...[leading, const SizedBox(width: 8)],
|
||||
Text(label, style: TextStyle(color: Colors.grey[600])),
|
||||
],
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: valueColor,
|
||||
),
|
||||
style: TextStyle(fontWeight: FontWeight.w500, color: valueColor),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
@@ -413,9 +432,23 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final l10n = context.l10n;
|
||||
final latController = TextEditingController();
|
||||
final lonController = TextEditingController();
|
||||
final intervalController = TextEditingController();
|
||||
latController.text = connector.selfLatitude?.toStringAsFixed(6) ?? '';
|
||||
lonController.text = connector.selfLongitude?.toStringAsFixed(6) ?? '';
|
||||
|
||||
// Safe access to custom vars - may be null before device responds
|
||||
final customVars = connector.currentCustomVars ?? {};
|
||||
final bool hasGPS = customVars.containsKey("gps");
|
||||
bool isGPSEnabled = customVars["gps"] == "1";
|
||||
|
||||
// Read current interval or default to 900 (15 minutes)
|
||||
final currentInterval = int.tryParse(customVars["gps_interval"] ?? "") ?? 900;
|
||||
intervalController.text = currentInterval.toString();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
builder: (context, setDialogState) => AlertDialog(
|
||||
title: Text(l10n.settings_location),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -426,7 +459,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
labelText: l10n.settings_latitude,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true),
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
signed: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
@@ -435,8 +471,39 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
labelText: l10n.settings_longitude,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true),
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
signed: true,
|
||||
),
|
||||
),
|
||||
if (hasGPS) ...[
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: intervalController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.settings_locationIntervalSec,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: false,
|
||||
signed: false,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FeatureToggleRow(
|
||||
title: l10n.settings_locationGPSEnable,
|
||||
subtitle: l10n.settings_locationGPSEnableSubtitle,
|
||||
value: isGPSEnabled,
|
||||
onChanged: (value) async {
|
||||
setDialogState(() => isGPSEnabled = value);
|
||||
if (value) {
|
||||
await connector.setCustomVar("gps:1");
|
||||
} else {
|
||||
await connector.setCustomVar("gps:0");
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
@@ -447,6 +514,32 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
|
||||
if (hasGPS) {
|
||||
final intervalText = intervalController.text.trim();
|
||||
if (intervalText.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final interval = int.tryParse(intervalText);
|
||||
if (interval == null || interval < 60 || interval >= 86400) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.settings_locationIntervalInvalid),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await connector.setCustomVar("gps_interval:$interval");
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_locationUpdated)),
|
||||
);
|
||||
}
|
||||
|
||||
final latText = latController.text.trim();
|
||||
final lonText = lonController.text.trim();
|
||||
if (latText.isEmpty && lonText.isEmpty) {
|
||||
@@ -455,8 +548,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
|
||||
final currentLat = connector.selfLatitude;
|
||||
final currentLon = connector.selfLongitude;
|
||||
final lat = latText.isNotEmpty ? double.tryParse(latText) : currentLat;
|
||||
final lon = lonText.isNotEmpty ? double.tryParse(lonText) : currentLon;
|
||||
final lat = latText.isNotEmpty
|
||||
? double.tryParse(latText)
|
||||
: currentLat;
|
||||
final lon = lonText.isNotEmpty
|
||||
? double.tryParse(lonText)
|
||||
: currentLon;
|
||||
if (lat == null || lon == null) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -483,6 +580,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -530,17 +628,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
void _sendAdvert(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
connector.sendSelfAdvert(flood: true);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_advertisementSent)),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.settings_advertisementSent)));
|
||||
}
|
||||
|
||||
void _syncTime(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
connector.syncTime();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_timeSynchronized)),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.settings_timeSynchronized)));
|
||||
}
|
||||
|
||||
void _confirmReboot(BuildContext context, MeshCoreConnector connector) {
|
||||
@@ -560,7 +658,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Navigator.pop(context);
|
||||
connector.rebootDevice();
|
||||
},
|
||||
child: Text(l10n.common_reboot, style: const TextStyle(color: Colors.orange)),
|
||||
child: Text(
|
||||
l10n.common_reboot,
|
||||
style: const TextStyle(color: Colors.orange),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -572,7 +673,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
showAboutDialog(
|
||||
context: context,
|
||||
applicationName: l10n.appTitle,
|
||||
applicationVersion: _appVersion.isEmpty ? l10n.common_loading : _appVersion,
|
||||
applicationVersion: _appVersion.isEmpty
|
||||
? l10n.common_loading
|
||||
: _appVersion,
|
||||
applicationLegalese: l10n.settings_aboutLegalese,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
@@ -604,7 +707,8 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
|
||||
// Populate with current settings if available
|
||||
if (widget.connector.currentFreqHz != null) {
|
||||
_frequencyController.text = (widget.connector.currentFreqHz! / 1000.0).toStringAsFixed(3);
|
||||
_frequencyController.text = (widget.connector.currentFreqHz! / 1000.0)
|
||||
.toStringAsFixed(3);
|
||||
} else {
|
||||
_frequencyController.text = '915.0';
|
||||
}
|
||||
@@ -670,26 +774,31 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
final txPower = int.tryParse(_txPowerController.text);
|
||||
|
||||
if (freqMHz == null || freqMHz < 300 || freqMHz > 2500) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_frequencyInvalid)),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.settings_frequencyInvalid)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (txPower == null || txPower < 0 || txPower > 22) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_txPowerInvalid)),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.settings_txPowerInvalid)));
|
||||
return;
|
||||
}
|
||||
|
||||
final freqHz = (freqMHz * 1000).round();
|
||||
final bwHz = _bandwidth.hz;
|
||||
final sf = _spreadingFactor.value;
|
||||
final cr = _toDeviceCodingRate(_codingRate.value, widget.connector.currentCr);
|
||||
final cr = _toDeviceCodingRate(
|
||||
_codingRate.value,
|
||||
widget.connector.currentCr,
|
||||
);
|
||||
|
||||
try {
|
||||
await widget.connector.sendFrame(buildSetRadioParamsFrame(freqHz, bwHz, sf, cr));
|
||||
await widget.connector.sendFrame(
|
||||
buildSetRadioParamsFrame(freqHz, bwHz, sf, cr),
|
||||
);
|
||||
await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower));
|
||||
await widget.connector.refreshDeviceInfo();
|
||||
|
||||
@@ -727,7 +836,10 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.settings_presets, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text(
|
||||
l10n.settings_presets,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
@@ -762,7 +874,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
border: const OutlineInputBorder(),
|
||||
helperText: l10n.settings_frequencyHelper,
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<LoRaBandwidth>(
|
||||
@@ -772,10 +886,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: LoRaBandwidth.values
|
||||
.map((bw) => DropdownMenuItem(
|
||||
value: bw,
|
||||
child: Text(bw.label),
|
||||
))
|
||||
.map(
|
||||
(bw) => DropdownMenuItem(value: bw, child: Text(bw.label)),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) setState(() => _bandwidth = value);
|
||||
@@ -789,10 +902,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: LoRaSpreadingFactor.values
|
||||
.map((sf) => DropdownMenuItem(
|
||||
value: sf,
|
||||
child: Text(sf.label),
|
||||
))
|
||||
.map(
|
||||
(sf) => DropdownMenuItem(value: sf, child: Text(sf.label)),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) setState(() => _spreadingFactor = value);
|
||||
@@ -806,10 +918,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: LoRaCodingRate.values
|
||||
.map((cr) => DropdownMenuItem(
|
||||
value: cr,
|
||||
child: Text(cr.label),
|
||||
))
|
||||
.map(
|
||||
(cr) => DropdownMenuItem(value: cr, child: Text(cr.label)),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) setState(() => _codingRate = value);
|
||||
@@ -833,10 +944,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(l10n.common_cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: _saveSettings,
|
||||
child: Text(l10n.common_save),
|
||||
),
|
||||
FilledButton(onPressed: _saveSettings, child: Text(l10n.common_save)),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -850,9 +958,6 @@ class _PresetChip extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ActionChip(
|
||||
label: Text(label),
|
||||
onPressed: onTap,
|
||||
);
|
||||
return ActionChip(label: Text(label), onPressed: onTap);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@ class BleDebugLogEntry {
|
||||
|
||||
String get hexPreview {
|
||||
const maxBytes = 64;
|
||||
final bytes = payload.length > maxBytes ? payload.sublist(0, maxBytes) : payload;
|
||||
final bytes = payload.length > maxBytes
|
||||
? payload.sublist(0, maxBytes)
|
||||
: payload;
|
||||
final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ');
|
||||
return payload.length > maxBytes ? '$hex …' : hex;
|
||||
}
|
||||
@@ -26,14 +28,13 @@ class BleRawLogRxEntry {
|
||||
final DateTime timestamp;
|
||||
final Uint8List payload;
|
||||
|
||||
BleRawLogRxEntry({
|
||||
required this.timestamp,
|
||||
required this.payload,
|
||||
});
|
||||
BleRawLogRxEntry({required this.timestamp, required this.payload});
|
||||
|
||||
String get hexPreview {
|
||||
const maxBytes = 64;
|
||||
final bytes = payload.length > maxBytes ? payload.sublist(0, maxBytes) : payload;
|
||||
final bytes = payload.length > maxBytes
|
||||
? payload.sublist(0, maxBytes)
|
||||
: payload;
|
||||
final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ');
|
||||
return payload.length > maxBytes ? '$hex …' : hex;
|
||||
}
|
||||
@@ -45,7 +46,8 @@ class BleDebugLogService extends ChangeNotifier {
|
||||
final List<BleRawLogRxEntry> _rawLogRxEntries = [];
|
||||
|
||||
List<BleDebugLogEntry> get entries => List.unmodifiable(_entries);
|
||||
List<BleRawLogRxEntry> get rawLogRxEntries => List.unmodifiable(_rawLogRxEntries);
|
||||
List<BleRawLogRxEntry> get rawLogRxEntries =>
|
||||
List.unmodifiable(_rawLogRxEntries);
|
||||
|
||||
void logFrame(Uint8List frame, {required bool outgoing, String? note}) {
|
||||
if (frame.isEmpty) return;
|
||||
@@ -85,7 +87,12 @@ class BleDebugLogService extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String _describeFrame(int code, Uint8List frame, bool outgoing, String? note) {
|
||||
String _describeFrame(
|
||||
int code,
|
||||
Uint8List frame,
|
||||
bool outgoing,
|
||||
String? note,
|
||||
) {
|
||||
final label = _codeLabel(code, outgoing: outgoing);
|
||||
final prefix = outgoing ? 'TX' : 'RX';
|
||||
final extra = _frameDetail(code, frame);
|
||||
@@ -147,6 +154,8 @@ class BleDebugLogService extends ChangeNotifier {
|
||||
return 'CMD_SET_CHANNEL';
|
||||
case cmdGetRadioSettings:
|
||||
return 'CMD_GET_RADIO_SETTINGS';
|
||||
case cmdSetCustomVar:
|
||||
return 'CMD_SET_CUSTOM_VAR';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FeatureToggleRow extends StatefulWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final bool value;
|
||||
final bool hasRefreshing;
|
||||
final bool isRefreshing;
|
||||
final ValueChanged<bool>? onChanged;
|
||||
final VoidCallback? onRefresh;
|
||||
final String? refreshTooltip;
|
||||
|
||||
const FeatureToggleRow({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.value,
|
||||
this.hasRefreshing = false,
|
||||
this.isRefreshing = false,
|
||||
this.onChanged,
|
||||
this.onRefresh,
|
||||
this.refreshTooltip,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FeatureToggleRow> createState() => _FeatureToggleRow();
|
||||
}
|
||||
|
||||
class _FeatureToggleRow extends State<FeatureToggleRow> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SwitchListTile(
|
||||
title: Text(widget.title),
|
||||
subtitle: Text(widget.subtitle),
|
||||
value: widget.value,
|
||||
onChanged: widget.onChanged,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
if (widget.hasRefreshing)
|
||||
IconButton(
|
||||
icon: widget.isRefreshing
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.refresh, size: 20),
|
||||
onPressed: widget.isRefreshing ? null : widget.onRefresh,
|
||||
tooltip: widget.refreshTooltip,
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user