diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index ef2f9b7..ee9533e 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -257,6 +257,9 @@ class MeshCoreConnector extends ChangeNotifier { int? _activeChannelIndex; List _channelOrder = []; + int _storageUsedKb = -1; + int _storageTotalKb = -1; + // Getters MeshCoreConnectionState get state => _state; BluetoothDevice? get device => _device; @@ -329,10 +332,17 @@ class MeshCoreConnector extends ChangeNotifier { bool? get autoAddRoomServers => _autoAddRoomServers; bool? get autoAddSensors => _autoAddSensors; bool? get autoAddOverwriteOldest => _overwriteOldest; + int get telemetryModeBase => _telemetryModeBase; + int get telemetryModeLoc => _telemetryModeLoc; + int get telemetryModeEnv => _telemetryModeEnv; + int get advertLocationPolicy => _advertLocPolicy; + int get multiAcks => _multiAcks; bool? get clientRepeat => _clientRepeat; int? get firmwareVerCode => _firmwareVerCode; Map? get currentCustomVars => _currentCustomVars; int? get batteryMillivolts => _batteryMillivolts; + int? get storageUsedKb => _storageUsedKb; + int? get storageTotalKb => _storageTotalKb; int get maxContacts => _maxContacts; int get maxChannels => _maxChannels; Set get knownContactKeys => Set.unmodifiable(_knownContactKeys); @@ -1922,13 +1932,36 @@ class MeshCoreConnector extends ChangeNotifier { } } - Future setContactFavorite(Contact contact, bool isFavorite) async { + Future setContactFlags( + Contact contact, { + bool? isFavorite, + bool? teleBase, + bool? teleLoc, + bool? teleEnv, + }) async { if (!isConnected) return; final latestContact = await _fetchContactSnapshotFromDevice(contact.publicKey) ?? contact; - final updatedFlags = isFavorite - ? (latestContact.flags | contactFlagFavorite) - : (latestContact.flags & ~contactFlagFavorite); + int updatedFlags = isFavorite != null + ? (isFavorite + ? (latestContact.flags | contactFlagFavorite) + : (latestContact.flags & ~contactFlagFavorite)) + : latestContact.flags; + updatedFlags = teleBase != null + ? (teleBase + ? (updatedFlags | contactFlagTeleBase) + : (updatedFlags & ~contactFlagTeleBase)) + : updatedFlags; + updatedFlags = teleLoc != null + ? (teleLoc + ? (updatedFlags | contactFlagTeleLoc) + : (updatedFlags & ~contactFlagTeleLoc)) + : updatedFlags; + updatedFlags = teleEnv != null + ? (teleEnv + ? (updatedFlags | contactFlagTeleEnv) + : (updatedFlags & ~contactFlagTeleEnv)) + : updatedFlags; await sendFrame( buildUpdateContactPathFrame( @@ -2094,9 +2127,7 @@ class MeshCoreConnector extends ChangeNotifier { outboundText, selfKey, ); - final ackHashHex = ackHash - .map((b) => b.toRadixString(16).padLeft(2, '0')) - .join(); + final ackHashHex = ackHashToHex(ackHash); final messageBytes = utf8.encode(outboundText).length; _pendingRepeaterAcks[ackHashHex]?.timeout?.cancel(); _pendingRepeaterAcks[ackHashHex] = _RepeaterAckContext( @@ -2468,6 +2499,31 @@ class MeshCoreConnector extends ChangeNotifier { await sendCliCommand('set privacy ${enabled ? 'on' : 'off'}'); } + Future setTelemetryModeBase( + int base, + int location, + int env, + int advert, + int multiAcks, + ) async { + if (!isConnected) return; + _telemetryModeBase = base.clamp(teleModeDeny, teleModeAllowAll).toInt(); + _telemetryModeLoc = location.clamp(teleModeDeny, teleModeAllowAll).toInt(); + _telemetryModeEnv = env.clamp(teleModeDeny, teleModeAllowAll).toInt(); + _advertLocPolicy = advert.clamp(0, 1).toInt(); + _multiAcks = multiAcks.clamp(0, 2).toInt(); + await sendFrame( + buildSetOtherParamsFrame( + (_telemetryModeEnv << 4) | + (_telemetryModeLoc << 2) | + _telemetryModeBase, + _advertLocPolicy, + _multiAcks, + ), + ); + notifyListeners(); + } + Future getChannels({int? maxChannels, bool force = false}) async { if (!isConnected) return; if (_isSyncingChannels) { @@ -2843,7 +2899,7 @@ class MeshCoreConnector extends ChangeNotifier { _currentSf = reader.readByte(); _currentCr = reader.readByte(); - _selfName = reader.readString(); + _selfName = reader.readCString(); } catch (e) { _appDebugLogService?.error( 'Error parsing SELF_INFO frame: $e', @@ -2984,14 +3040,23 @@ class MeshCoreConnector extends ChangeNotifier { // [1-2] = battery_mv (uint16 LE) // [3-6] = storage_used_kb (uint32 LE) // [7-10] = storage_total_kb (uint32 LE) - if (frame.length >= 3) { - _batteryMillivolts = readUint16LE(frame, 1); + try { + final reader = BufferReader(frame); + reader.skipBytes(1); + _batteryMillivolts = reader.readUInt16LE(); + _storageUsedKb = reader.readUInt32LE(); + _storageTotalKb = reader.readUInt32LE(); final volts = (_batteryMillivolts! / 1000.0).toStringAsFixed(2); _appDebugLogService?.info( 'Pulled battery: $volts V ($_batteryMillivolts mV)', tag: 'Battery', ); notifyListeners(); + } catch (e) { + _appDebugLogService?.error( + 'Error parsing battery and storage frame: $e', + tag: 'Connector', + ); } } @@ -3487,7 +3552,7 @@ class MeshCoreConnector extends ChangeNotifier { reader.skipBytes(4); // Skip extra 4 bytes for signed/plain variants } - final msgText = reader.readString(); + final msgText = reader.readCString(); final flags = txtType; final shiftedType = flags >> 2; @@ -3649,68 +3714,87 @@ class MeshCoreConnector extends ChangeNotifier { void _handleLogRxData(Uint8List frame) { if (frame.length < 4) return; - final raw = Uint8List.fromList(frame.sublist(3)); - final packet = _parseRawPacket(raw); - if (packet == null || packet.payloadType != _payloadTypeGroupText) return; + try { + final reader = BufferReader(frame); + reader.skipBytes(3); // Skip header - final payload = packet.payload; - if (payload.length <= _cipherMacSize) return; - final channelHash = payload[0]; - final encrypted = Uint8List.fromList(payload.sublist(1)); + final raw = reader.readRemainingBytes(); + final packet = _parseRawPacket(raw); + if (packet == null || packet.payloadType != _payloadTypeGroupText) return; - // Use cached channels as fallback if live channels not yet loaded - final channelsToSearch = _channels.isNotEmpty ? _channels : _cachedChannels; - for (final channel in channelsToSearch) { - if (channel.isEmpty) continue; - final hash = _computeChannelHash(channel.psk); - if (hash != channelHash) continue; + final payload = BufferReader(packet.payload); + final channelHash = payload.readByte(); + final encrypted = Uint8List.fromList(payload.readRemainingBytes()); - final decrypted = _decryptPayload(channel.psk, encrypted); - if (decrypted == null || decrypted.length < 6) return; + // Use cached channels as fallback if live channels not yet loaded + final channelsToSearch = _channels.isNotEmpty + ? _channels + : _cachedChannels; + for (final channel in channelsToSearch) { + if (channel.isEmpty) continue; + final hash = _computeChannelHash(channel.psk); + if (hash != channelHash) continue; + try { + final decryptedBytes = _decryptPayload(channel.psk, encrypted); + if (decryptedBytes == null || decryptedBytes.length < 6) return; + final decrypted = BufferReader(decryptedBytes); - final txtType = decrypted[4]; - if ((txtType >> 2) != 0) { - return; + final timestampRaw = decrypted.readUInt32LE(); + final txtType = decrypted.readByte(); + if ((txtType >> 2) != 0) { + return; + } + + final text = decrypted.readCString(); + final parsed = _splitSenderText(text); + final decodedText = + Smaz.tryDecodePrefixed(parsed.text) ?? parsed.text; + if (_shouldDropSelfChannelMessage( + parsed.senderName, + packet.pathBytes, + )) { + return; + } + + final pktHash = _computePacketHash( + packet.payloadType, + packet.payload, + ); + + final message = ChannelMessage( + senderKey: null, + senderName: parsed.senderName, + text: decodedText, + timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000), + isOutgoing: false, + status: ChannelMessageStatus.sent, + pathLength: packet.isFlood ? packet.hopCount : 0, + pathBytes: packet.pathBytes, + channelIndex: channel.index, + packetHash: pktHash, + ); + + _updateContactLastMessageAtByName( + parsed.senderName, + message.timestamp, + pathBytes: message.pathBytes, + ); + final isNew = _addChannelMessage(channel.index, message); + _maybeIncrementChannelUnread(message, isNew: isNew); + notifyListeners(); + if (isNew) { + final label = channel.name.isEmpty + ? 'Channel ${channel.index}' + : channel.name; + _maybeNotifyChannelMessage(message, channelName: label); + } + return; + } catch (e) { + appLogger.warn('Decryption failed for channel ${channel.index}: $e'); + } } - - final timestampRaw = readUint32LE(decrypted, 0); - final text = readCString(decrypted, 5, decrypted.length - 5); - final parsed = _splitSenderText(text); - final decodedText = Smaz.tryDecodePrefixed(parsed.text) ?? parsed.text; - if (_shouldDropSelfChannelMessage(parsed.senderName, packet.pathBytes)) { - return; - } - - final pktHash = _computePacketHash(packet.payloadType, packet.payload); - - final message = ChannelMessage( - senderKey: null, - senderName: parsed.senderName, - text: decodedText, - timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000), - isOutgoing: false, - status: ChannelMessageStatus.sent, - pathLength: packet.isFlood ? packet.hopCount : 0, - pathBytes: packet.pathBytes, - channelIndex: channel.index, - packetHash: pktHash, - ); - - _updateContactLastMessageAtByName( - parsed.senderName, - message.timestamp, - pathBytes: message.pathBytes, - ); - final isNew = _addChannelMessage(channel.index, message); - _maybeIncrementChannelUnread(message, isNew: isNew); - notifyListeners(); - if (isNew) { - final label = channel.name.isEmpty - ? 'Channel ${channel.index}' - : channel.name; - _maybeNotifyChannelMessage(message, channelName: label); - } - return; + } catch (e) { + appLogger.warn('Error handling log RX data frame: $e'); } } @@ -3721,15 +3805,15 @@ class MeshCoreConnector extends ChangeNotifier { // [2-5] = expected_ack_hash (uint32) // [6-9] = estimated_timeout_ms (uint32) - if (frame.length >= 10) { - final ackHash = Uint8List.fromList(frame.sublist(2, 6)); - final timeoutMs = readUint32LE(frame, 6); + try { + final reader = BufferReader(frame); + reader.skipBytes(2); //Skip code and is_flood + final ackHash = reader.readUInt32LE(); + final timeoutMs = reader.readUInt32LE(); // 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 = ackHashToHex(ackHash); debugPrint('Ignoring CLI command ACK (sent): $ackHashHex'); _lastSentWasCliCommand = false; return; @@ -3748,7 +3832,8 @@ class MeshCoreConnector extends ChangeNotifier { if (_markNextPendingChannelMessageSent()) { return; } - } else { + } catch (e) { + appLogger.warn('Error handling message sent frame: $e'); // Fallback to old behavior for (var messages in _conversations.values) { for (int i = messages.length - 1; i >= 0; i--) { @@ -3827,9 +3912,11 @@ class MeshCoreConnector extends ChangeNotifier { // [1-4] = ack_hash (uint32) // [5-8] = trip_time_ms (uint32) - if (frame.length >= 9) { - final ackHash = Uint8List.fromList(frame.sublist(1, 5)); - final tripTimeMs = readUint32LE(frame, 5); + try { + final reader = BufferReader(frame); + reader.skipBytes(1); // Skip code + final ackHash = reader.readUInt32LE(); + final tripTimeMs = reader.readUInt32LE(); // CLI command ACKs are already filtered in _handleMessageSent, so this should only see real messages @@ -3841,7 +3928,8 @@ class MeshCoreConnector extends ChangeNotifier { if (_retryService != null) { _retryService!.handleAckReceived(ackHash, tripTimeMs); } - } else { + } catch (e) { + appLogger.warn('Error handling send confirmed frame: $e'); // Fallback to old behavior for (var messages in _conversations.values) { for (int i = messages.length - 1; i >= 0; i--) { @@ -3856,10 +3944,8 @@ class MeshCoreConnector extends ChangeNotifier { } } - bool _handleRepeaterCommandSent(Uint8List ackHash, int timeoutMs) { - final ackHashHex = ackHash - .map((b) => b.toRadixString(16).padLeft(2, '0')) - .join(); + bool _handleRepeaterCommandSent(int ackHash, int timeoutMs) { + final ackHashHex = ackHashToHex(ackHash); final entry = _pendingRepeaterAcks[ackHashHex]; if (entry == null) return false; @@ -3877,10 +3963,8 @@ class MeshCoreConnector extends ChangeNotifier { return true; } - bool _handleRepeaterCommandAck(Uint8List ackHash, int tripTimeMs) { - final ackHashHex = ackHash - .map((b) => b.toRadixString(16).padLeft(2, '0')) - .join(); + bool _handleRepeaterCommandAck(int ackHash, int tripTimeMs) { + final ackHashHex = ackHashToHex(ackHash); final entry = _pendingRepeaterAcks.remove(ackHashHex); if (entry == null) return false; entry.timeout?.cancel(); @@ -4231,36 +4315,35 @@ class MeshCoreConnector extends ChangeNotifier { } _RawPacket? _parseRawPacket(Uint8List raw) { - if (raw.length < 3) return null; - var index = 0; - final header = raw[index++]; - final routeType = header & _phRouteMask; - final hasTransport = - routeType == _routeTransportFlood || routeType == _routeTransportDirect; - if (hasTransport) { - if (raw.length < index + 4) return null; - index += 4; - } - if (raw.length <= index) return null; - final pathLenRaw = raw[index++]; - final pathByteLen = _decodePathByteLen(pathLenRaw); - if (raw.length < index + pathByteLen) return null; - final pathBytes = Uint8List.fromList( - raw.sublist(index, index + pathByteLen), - ); - index += pathByteLen; - if (raw.length <= index) return null; - final payload = Uint8List.fromList(raw.sublist(index)); + try { + final reader = BufferReader(raw); + final header = reader.readByte(); + final routeType = header & _phRouteMask; + final hasTransport = + routeType == _routeTransportFlood || + routeType == _routeTransportDirect; + if (hasTransport) { + // Skip reserved bytes in transport header made up of two u16 fields + reader.skipBytes(4); + } + final pathLenRaw = reader.readByte(); + final pathByteLen = _decodePathByteLen(pathLenRaw); + final pathBytes = reader.readBytes(pathByteLen); + final payload = reader.readBytes(reader.remaining); - return _RawPacket( - header: header, - routeType: routeType, - payloadType: (header >> _phTypeShift) & _phTypeMask, - payloadVer: (header >> _phVerShift) & _phVerMask, - pathLenRaw: pathLenRaw, - pathBytes: pathBytes, - payload: payload, - ); + return _RawPacket( + header: header, + routeType: routeType, + payloadType: (header >> _phTypeShift) & _phTypeMask, + payloadVer: (header >> _phVerShift) & _phVerMask, + pathLenRaw: pathLenRaw, + pathBytes: pathBytes, + payload: payload, + ); + } catch (e) { + appLogger.warn('Error parsing raw packet: $e'); + return null; + } } int _computeChannelHash(Uint8List psk) { @@ -4679,7 +4762,7 @@ class MeshCoreConnector extends ChangeNotifier { void _handleCustomVars(Uint8List frame) { final buf = BufferReader(frame.sublist(1)); try { - _currentCustomVars = _parseKeyValueString(buf.readString()); + _currentCustomVars = _parseKeyValueString(buf.readCString()); } catch (e) { appLogger.warn('Malformed custom vars frame: $e', tag: 'Connector'); } @@ -4836,7 +4919,7 @@ class MeshCoreConnector extends ChangeNotifier { longitude = packet.readInt32LE() / 1e6; } if (hasName && packet.remaining > 0) { - name = packet.readString(); + name = packet.readCString(); } } catch (e) { appLogger.warn('Malformed advert frame: $e', tag: 'Connector'); @@ -4898,7 +4981,7 @@ class MeshCoreConnector extends ChangeNotifier { longitude = advert.readInt32LE() / 1e6; } if (hasName && advert.remaining > 0) { - name = advert.readString(); + name = advert.readCString(); } } catch (e) { appLogger.warn('Malformed advert frame: $e', tag: 'Connector'); @@ -5124,6 +5207,25 @@ class MeshCoreConnector extends ChangeNotifier { unawaited(_persistDiscoveredContacts()); notifyListeners(); } + + void clearMessagesForContact(Contact contact) { + final contactKeyHex = contact.publicKeyHex; + final messages = _conversations[contactKeyHex]; + if (messages == null) return; + messages.clear(); + unawaited(_messageStore.saveMessages(contactKeyHex, messages)); + markContactRead(contactKeyHex); + notifyListeners(); + } + + void clearMessagesForChannel(int channelIndex) { + final messages = _channelMessages[channelIndex]; + if (messages == null) return; + messages.clear(); + unawaited(_channelMessageStore.saveChannelMessages(channelIndex, messages)); + markChannelRead(channelIndex); + notifyListeners(); + } } const int _phRouteMask = 0x03; diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 1a0ada1..b368756 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:flutter/widgets.dart'; + // Buffer Reader - sequential binary data reader with pointer tracking class BufferReader { int _pointer = 0; @@ -37,16 +39,6 @@ class BufferReader { Uint8List readRemainingBytes() => readBytes(remaining); - String readString() { - _lastPointer = _pointer; - final value = readRemainingBytes(); - try { - return utf8.decode(Uint8List.fromList(value), allowMalformed: true); - } catch (e) { - return String.fromCharCodes(value); // Latin-1 fallback - } - } - String readCStringGreedy(int maxLength) { _lastPointer = _pointer; final value = []; @@ -62,11 +54,12 @@ class BufferReader { } } - String readCString(int maxLength) { + String readCString({int maxLength = -1}) { final backupPointer = _pointer; final value = []; int counter = 0; - while (counter < maxLength) { + final maxLen = maxLength >= 0 ? maxLength : remaining; + while (counter < maxLen) { final byte = readByte(); if (byte == 0) break; value.add(byte); @@ -210,7 +203,7 @@ const int cmdSetChannel = 32; const int cmdSendTracePath = 36; const int cmdSetOtherParams = 38; const int cmdSendAnonReq = 57; -const int cmdGetTelemetryReq = 39; +const int cmdSendTelemetryReq = 39; const int cmdGetCustomVar = 40; const int cmdSetCustomVar = 41; const int cmdSendBinaryReq = 50; @@ -220,6 +213,7 @@ const int cmdGetAutoAddConfig = 59; // Text message types const int txtTypePlain = 0; const int txtTypeCliData = 1; +const int txtTypeSigned = 2; // Repeater request types (for server requests) const int reqTypeGetStatus = 0x01; @@ -272,6 +266,10 @@ const int advTypeRepeater = 2; const int advTypeRoom = 3; const int advTypeSensor = 4; +const int teleModeDeny = 0; +const int teleModeAllowFlags = 1; // use contact.flags +const int teleModeAllowAll = 2; + // Payload Types const int payloadTypeREQ = 0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob) @@ -310,6 +308,7 @@ const int autoAddSensorFlag = // Sizes const int pubKeySize = 32; +const int signatureSize = 64; const int maxPathSize = 64; const int pathHashSize = 1; const int maxNameSize = 32; @@ -352,6 +351,9 @@ const int contactPubKeyOffset = 1; const int contactTypeOffset = 33; const int contactFlagsOffset = 34; const int contactFlagFavorite = 0x01; +const int contactFlagTeleBase = 0x02; // 'base' permission includes battery +const int contactFlagTeleLoc = 0x04; +const int contactFlagTeleEnv = 0x08; //access environment sensors const int contactPathLenOffset = 35; const int contactPathOffset = 36; const int contactNameOffset = 100; @@ -370,52 +372,44 @@ const int msgTextOffset = 38; class ParsedContactText { final Uint8List senderPrefix; final String text; - const ParsedContactText({required this.senderPrefix, required this.text}); } ParsedContactText? parseContactMessageText(Uint8List frame) { if (frame.isEmpty) return null; - final code = frame[0]; - if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) { + + final message = BufferReader(frame); + try { + final code = message.readByte(); + if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) { + return null; + } + + // Companion radio layout: + // [code][snr?][res?][res?][prefix x6][path_len][txt_type][timestamp x4][extra?][text...] + if (code == respCodeContactMsgRecvV3) { + // Skip SNR and reserved bytes in v3 layout + message.skipBytes(3); + } + final senderPrefix = message.readBytes(6); // public key + message.skipBytes(1); // path length + final textType = message.readByte(); + message.skipBytes(4); // timestamp (4 bytes) + + final shiftedType = textType >> 2; + final isSigned = shiftedType == txtTypeSigned || textType == txtTypeSigned; + if (isSigned) { + // Signed messages have a 4-byte signature after the timestamp, before the text + message.skipBytes(4); + } + final text = message.readCString(); + if (text.isEmpty) return null; + + return ParsedContactText(senderPrefix: senderPrefix, text: text); + } catch (e) { + debugPrint('Error parsing contact message text: $e'); return null; } - - // Companion radio layout: - // [code][snr?][res?][res?][prefix x6][path_len][txt_type][timestamp x4][extra?][text...] - final isV3 = code == respCodeContactMsgRecvV3; - final prefixOffset = isV3 ? 4 : 1; - const prefixLen = 6; - final txtTypeOffset = prefixOffset + prefixLen + 1; - final timestampOffset = txtTypeOffset + 1; - final baseTextOffset = timestampOffset + 4; - if (frame.length <= baseTextOffset) return null; - - final flags = frame[txtTypeOffset]; - final shiftedType = flags >> 2; - final rawType = flags; - final isPlain = shiftedType == txtTypePlain || rawType == txtTypePlain; - final isCli = shiftedType == txtTypeCliData || rawType == txtTypeCliData; - if (!isPlain && !isCli) { - return null; - } - - 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(); - } - if (text.isEmpty) return null; - - final senderPrefix = frame.sublist(prefixOffset, prefixOffset + prefixLen); - return ParsedContactText(senderPrefix: senderPrefix, text: text); } // Helper to read uint32 little-endian @@ -438,18 +432,9 @@ int readInt32LE(Uint8List data, int offset) { return val; } -// Helper to read null-terminated UTF-8 string -String readCString(Uint8List data, int offset, int maxLen) { - int end = offset; - while (end < offset + maxLen && end < data.length && data[end] != 0) { - end++; - } - try { - return utf8.decode(data.sublist(offset, end), allowMalformed: true); - } catch (e) { - // Fallback to Latin-1 if UTF-8 decoding fails - return String.fromCharCodes(data.sublist(offset, end)); - } +// Helper to convert uint32 to hex string +String ackHashToHex(int ackHash) { + return ackHash.toRadixString(16).padLeft(8, '0'); } // Helper to convert public key to hex string @@ -937,3 +922,18 @@ Uint8List buildSetAutoAddConfigFrame({ writer.writeByte(flags); return writer.toBytes(); } + +//Build CMD_SEND_TELEMETRY_REQ +// Format: [cmd][reserved x3][pub_key? x32] +Uint8List buildSendTelemetryReq(Uint8List? pubKey) { + final writer = BufferWriter(); + writer.writeByte(cmdSendTelemetryReq); + + if (pubKey != null && pubKey.length == pubKeySize) { + writer.writeBytes(Uint8List(3)); // reserved bytes + writer.writeBytes(pubKey); + } else { + writer.writeBytes(Uint8List(4)); // reserved bytes + } + return writer.toBytes(); +} diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index b8ea08f..545ec9d 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1900,6 +1900,35 @@ } } }, + "settings_denyAll": "Откажи всичко", + "settings_allowAll": "Позволи всичко", + "settings_allowByContact": "Позволи по флагове за контакт", + "settings_privacy": "Настройки на поверителността", + "settings_privacySettingsDescription": "Изберете каква информация устройството ви споделя с другите.", + "settings_privacySubtitle": "Контролирайте каква информация се споделя.", + "settings_telemetryBaseMode": "Базов режим на телеметрия", + "settings_telemetryLocationMode": "Режим на местоположение на телеметрията", + "settings_advertLocation": "Място на обявата", + "settings_advertLocationSubtitle": "Включи местоположение в обявата", + "contact_info": "Контактна информация", + "settings_telemetryEnvironmentMode": "Режим на средата на телеметрията", + "contact_telemetry": "Телеметрия", + "contact_lastSeen": "Последно видян", + "contact_clearChat": "Изчисти чата", + "contact_teleBase": "Базата данни за телеметрия", + "contact_settings": "Настройки за контакти", + "contact_teleBaseSubtitle": "Позволи споделяне на ниво на батерията и основна телеметрия", + "contact_teleEnv": "Среда на телеметрия", + "contact_teleLocSubtitle": "Позволи споделяне на данни за местоположение", + "contact_teleLoc": "Местоположение на телеметрията", + "contact_teleEnvSubtitle": "Позволи споделяне на данни от средносферните датчици", + "@settings_multiAck": { + "placeholders": { + "value": { + "type": "String" + } + } + }, "appSettings_initialRouteWeight": "Първоначална тежест на маршрута", "appSettings_maxRouteWeight": "Максимално допустимо тегло на маршрута", "appSettings_initialRouteWeightSubtitle": "Начално тегло за новооткрити маршрути", @@ -1910,5 +1939,7 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Тегло, което е било премахнато от пътя след неуспешен опит за доставка.", "appSettings_maxMessageRetries": "Максимален брой опити за изпращане на съобщение", "appSettings_maxMessageRetriesSubtitle": "Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.", - "path_routeWeight": "{weight}/{max}" -} + "path_routeWeight": "{weight}/{max}", + "settings_multiAck": "Мулти-потвърди: {value}", + "settings_telemetryModeUpdated": "Режим на телеметрията е обновен" +} \ No newline at end of file diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 681cff6..6745054 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1928,6 +1928,35 @@ } } }, + "settings_allowByContact": "Zulassen durch Kontaktflaggen", + "settings_privacy": "Datenschutzeinstellungen", + "settings_allowAll": "Alles zulassen", + "settings_privacySettingsDescription": "Wählen Sie die Informationen, die Ihr Gerät mit anderen teilt.", + "settings_denyAll": "Alle ablehnen", + "settings_privacySubtitle": "Steuern Sie die Informationen, die freigegeben werden.", + "settings_telemetryLocationMode": "Telemetrie-Ortsmodus", + "settings_telemetryEnvironmentMode": "Telemetrie-Umgebungsmodus", + "settings_advertLocation": "Anzeigenort", + "settings_advertLocationSubtitle": "Ort in der Anzeige einbeziehen", + "settings_telemetryBaseMode": "Telemetrie-Basismodus", + "contact_teleBase": "Telemetriebasis", + "contact_teleBaseSubtitle": "Erlauben des Freigebens des Batteriestands und der grundlegenden Telemetrie", + "contact_teleLoc": "Telemetrieort", + "contact_teleLocSubtitle": "Teilen von Standortdaten zulassen", + "contact_info": "Kontaktinformationen", + "contact_settings": "Kontakteinstellungen", + "contact_telemetry": "Telemetrie", + "contact_teleEnv": "Telemetrieumgebung", + "contact_lastSeen": "Zuletzt gesehen", + "contact_clearChat": "Chat löschen", + "contact_teleEnvSubtitle": "Teilen von Umgebungsensordaten zulassen", + "@settings_multiAck": { + "placeholders": { + "value": { + "type": "String" + } + } + }, "appSettings_initialRouteWeightSubtitle": "Ausgangsgewicht für neu entdeckte Pfade", "appSettings_maxRouteWeightSubtitle": "Maximales Gewicht, das ein Weg durch erfolgreiche Lieferungen erreichen kann.", "appSettings_maxRouteWeight": "Maximale Gesamtstreckenlänge", @@ -1938,5 +1967,7 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Gewicht, das nach einem fehlgeschlagenen Versand von einem Weg entfernt wurde", "appSettings_maxMessageRetries": "Maximale Anzahl an Wiederholungsversuchen", "appSettings_maxMessageRetriesSubtitle": "Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.", - "path_routeWeight": "{weight}/{max}" -} + "path_routeWeight": "{weight}/{max}", + "settings_telemetryModeUpdated": "Telemetriemodus aktualisiert", + "settings_multiAck": "Mehrfach-Bestätigungen: {value}" +} \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 3942afb..2b263c6 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -166,6 +166,26 @@ "settings_privacyModeToggle": "Toggle privacy mode to hide your name and location in advertisements.", "settings_privacyModeEnabled": "Privacy mode enabled", "settings_privacyModeDisabled": "Privacy mode disabled", + "settings_privacy": "Privacy Settings", + "settings_privacySubtitle": "Control what information is shared.", + "settings_privacySettingsDescription": "Choose what information your device shares with others.", + "settings_denyAll": "Deny all", + "settings_allowByContact": "Allow by contact flags", + "settings_allowAll": "Allow all", + "settings_telemetryBaseMode": "Telemetry Base Mode", + "settings_telemetryLocationMode": "Telemetry Location Mode", + "settings_telemetryEnvironmentMode": "Telemetry Environment Mode", + "settings_advertLocation": "Advert Location", + "settings_advertLocationSubtitle": "Include location in advert.", + "settings_multiAck": "Multi-ACKs: {value}", + "@settings_multiAck": { + "placeholders": { + "value": { + "type": "String" + } + } + }, + "settings_telemetryModeUpdated": "Telemetry mode updated", "settings_actions": "Actions", "settings_sendAdvertisement": "Send Advertisement", "settings_sendAdvertisementSubtitle": "Broadcast presence now", @@ -472,6 +492,17 @@ } } }, + "contact_info": "Contact Info", + "contact_settings": "Contact Settings", + "contact_telemetry": "Telemetry", + "contact_lastSeen": "Last seen", + "contact_clearChat": "Clear Chat", + "contact_teleBase": "Telemetry Base", + "contact_teleBaseSubtitle": "Allow sharing battery level and basic telemetry", + "contact_teleLoc": "Telemetry Location", + "contact_teleLocSubtitle": "Allow sharing location data", + "contact_teleEnv": "Telemetry Environment", + "contact_teleEnvSubtitle": "Allow sharing environment sensor data", "channels_title": "Channels", "channels_noChannelsConfigured": "No channels configured", "channels_addPublicChannel": "Add Public Channel", @@ -1945,4 +1976,4 @@ "discoveredContacts_deleteContact": "Delete Discovered Contact", "discoveredContacts_deleteContactAll": "Delete All Discovered Contacts", "discoveredContacts_deleteContactAllContent": "Are you sure you want to delete all discovered contacts?" -} +} \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 4a83680..4a49e1b 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1928,6 +1928,35 @@ } } }, + "settings_privacySubtitle": "Controlar qué información se comparte.", + "settings_allowByContact": "Permitir por banderas de contacto", + "settings_denyAll": "Denegar todo", + "settings_telemetryBaseMode": "Modo base de telemetría", + "settings_telemetryEnvironmentMode": "Modo de entorno de telemetría", + "settings_advertLocationSubtitle": "Incluir ubicación en anuncio", + "contact_info": "Información de contacto", + "settings_privacySettingsDescription": "Elige qué información comparte tu dispositivo con otros.", + "settings_allowAll": "Permitir todo", + "settings_privacy": "Configuración de privacidad", + "contact_settings": "Configuración de contacto", + "settings_telemetryLocationMode": "Modo de ubicación de telemetría", + "contact_teleBase": "Base de Telemetría", + "contact_teleLoc": "Ubicación de telemetría", + "settings_advertLocation": "Ubicación de anuncio", + "contact_teleLocSubtitle": "Permitir el intercambio de datos de ubicación", + "contact_clearChat": "Borrar chat", + "contact_telemetry": "Telemetría", + "contact_lastSeen": "Visto por última vez", + "contact_teleBaseSubtitle": "Permitir el intercambio de nivel de batería y telemetría básica", + "contact_teleEnv": "Entorno de Telemetría", + "contact_teleEnvSubtitle": "Permitir el intercambio de datos de sensores de entorno", + "@settings_multiAck": { + "placeholders": { + "value": { + "type": "String" + } + } + }, "appSettings_initialRouteWeight": "Peso inicial de la ruta", "appSettings_maxRouteWeight": "Peso máximo permitido para la ruta", "appSettings_initialRouteWeightSubtitle": "Peso inicial para rutas recién descubiertas", @@ -1938,5 +1967,7 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Peso retirado de un camino después de un intento de entrega fallido.", "appSettings_maxMessageRetries": "Número máximo de reintentos de envío de mensajes", "appSettings_maxMessageRetriesSubtitle": "Número de intentos de reintento antes de marcar un mensaje como fallido.", - "path_routeWeight": "{weight}/{max}" -} + "path_routeWeight": "{weight}/{max}", + "settings_telemetryModeUpdated": "Modo de telemetría actualizado", + "settings_multiAck": "Multi-ACKs: {value}" +} \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 1d684bb..e98e317 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1900,6 +1900,35 @@ } } }, + "settings_privacy": "Paramètres de confidentialité", + "settings_privacySubtitle": "Contrôlez les informations partagées", + "settings_telemetryLocationMode": "Mode d'emplacement de télémétrie", + "settings_telemetryEnvironmentMode": "Mode d'environnement de télémétrie", + "settings_advertLocation": "Emplacement de l'annonce", + "settings_advertLocationSubtitle": "Inclure l'emplacement dans l'annonce", + "settings_denyAll": "Refuser tout", + "settings_allowByContact": "Autoriser par drapeaux de contact", + "settings_privacySettingsDescription": "Choisissez les informations que votre appareil partage avec les autres.", + "settings_allowAll": "Autoriser tout", + "contact_info": "Informations de contact", + "settings_telemetryBaseMode": "Mode de base Télémétrie", + "contact_teleBase": "Base de télémétrie", + "contact_teleLoc": "Emplacement de télémétrie", + "contact_teleLocSubtitle": "Autoriser le partage des données de localisation", + "contact_teleEnv": "Environnement Télémétrie", + "contact_teleEnvSubtitle": "Autoriser le partage des données des capteurs d'environnement", + "contact_telemetry": "Télémétrie", + "contact_settings": "Paramètres de contact", + "contact_lastSeen": "Dernière fois vu", + "contact_clearChat": "Effacer la conversation", + "contact_teleBaseSubtitle": "Autoriser le partage du niveau de batterie et de la télémétrie de base", + "@settings_multiAck": { + "placeholders": { + "value": { + "type": "String" + } + } + }, "appSettings_maxRouteWeightSubtitle": "Poids maximal qu'un itinéraire peut accumuler grâce à des livraisons réussies.", "appSettings_initialRouteWeight": "Poids initial de l'itinéraire", "appSettings_maxRouteWeight": "Poids maximal autorisé pour le trajet", @@ -1910,5 +1939,7 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Poids retiré d'un itinéraire après une tentative de livraison infructueuse.", "appSettings_maxMessageRetries": "Nombre maximal de tentatives de récupération de messages", "appSettings_maxMessageRetriesSubtitle": "Nombre de tentatives de relance avant de marquer un message comme ayant échoué.", - "path_routeWeight": "{weight}/{max}" -} + "path_routeWeight": "{weight}/{max}", + "settings_multiAck": "Multi-ACKs : {value}", + "settings_telemetryModeUpdated": "Le mode télémétrie a été mis à jour" +} \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 55a1054..f11cde5 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1900,6 +1900,35 @@ } } }, + "settings_privacySettingsDescription": "Scegli le informazioni che il tuo dispositivo condivide con gli altri.", + "settings_allowByContact": "Consenti in base ai flag di contatto", + "settings_telemetryLocationMode": "Modalità di posizionamento telemetrico", + "settings_telemetryEnvironmentMode": "Modalità di ambiente di telemetria", + "settings_advertLocation": "Posizione dell'annuncio", + "settings_advertLocationSubtitle": "Includi la posizione nell'annuncio", + "settings_privacy": "Impostazioni sulla privacy", + "settings_denyAll": "Negare tutto", + "settings_privacySubtitle": "Controlla le informazioni che vengono condivise.", + "settings_allowAll": "Consenti tutto", + "contact_info": "Informazioni di Contatto", + "settings_telemetryBaseMode": "Modalità di base di telemetria", + "contact_teleBase": "Base di telemetria", + "contact_teleLoc": "Posizione telemetria", + "contact_teleLocSubtitle": "Consenti la condivisione dei dati di posizione", + "contact_clearChat": "Cancella chat", + "contact_telemetry": "Telemetria", + "contact_settings": "Impostazioni di contatto", + "contact_lastSeen": "Ultimo accesso", + "contact_teleBaseSubtitle": "Consenti la condivisione del livello della batteria e della telemetria di base", + "contact_teleEnvSubtitle": "Consenti la condivisione dei dati del sensore ambientale", + "contact_teleEnv": "Ambiente di telemetria", + "@settings_multiAck": { + "placeholders": { + "value": { + "type": "String" + } + } + }, "appSettings_initialRouteWeight": "Peso iniziale del percorso", "appSettings_initialRouteWeightSubtitle": "Peso di partenza per nuovi percorsi", "appSettings_maxRouteWeightSubtitle": "Il peso massimo che un percorso può accumulare grazie a consegne di successo.", @@ -1910,5 +1939,7 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Peso rimosso da un percorso dopo un tentativo di consegna fallito.", "appSettings_maxMessageRetries": "Numero massimo di tentativi di invio del messaggio", "appSettings_maxMessageRetriesSubtitle": "Numero di tentativi di riprova prima di considerare un messaggio come fallito.", - "path_routeWeight": "{weight}/{max}" -} + "path_routeWeight": "{weight}/{max}", + "settings_telemetryModeUpdated": "Modalità telemetria aggiornata", + "settings_multiAck": "Multi-ACKs: {value}" +} \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 84b5432..4bb6936 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -826,6 +826,84 @@ abstract class AppLocalizations { /// **'Privacy mode disabled'** String get settings_privacyModeDisabled; + /// No description provided for @settings_privacy. + /// + /// In en, this message translates to: + /// **'Privacy Settings'** + String get settings_privacy; + + /// No description provided for @settings_privacySubtitle. + /// + /// In en, this message translates to: + /// **'Control what information is shared.'** + String get settings_privacySubtitle; + + /// No description provided for @settings_privacySettingsDescription. + /// + /// In en, this message translates to: + /// **'Choose what information your device shares with others.'** + String get settings_privacySettingsDescription; + + /// No description provided for @settings_denyAll. + /// + /// In en, this message translates to: + /// **'Deny all'** + String get settings_denyAll; + + /// No description provided for @settings_allowByContact. + /// + /// In en, this message translates to: + /// **'Allow by contact flags'** + String get settings_allowByContact; + + /// No description provided for @settings_allowAll. + /// + /// In en, this message translates to: + /// **'Allow all'** + String get settings_allowAll; + + /// No description provided for @settings_telemetryBaseMode. + /// + /// In en, this message translates to: + /// **'Telemetry Base Mode'** + String get settings_telemetryBaseMode; + + /// No description provided for @settings_telemetryLocationMode. + /// + /// In en, this message translates to: + /// **'Telemetry Location Mode'** + String get settings_telemetryLocationMode; + + /// No description provided for @settings_telemetryEnvironmentMode. + /// + /// In en, this message translates to: + /// **'Telemetry Environment Mode'** + String get settings_telemetryEnvironmentMode; + + /// No description provided for @settings_advertLocation. + /// + /// In en, this message translates to: + /// **'Advert Location'** + String get settings_advertLocation; + + /// No description provided for @settings_advertLocationSubtitle. + /// + /// In en, this message translates to: + /// **'Include location in advert.'** + String get settings_advertLocationSubtitle; + + /// No description provided for @settings_multiAck. + /// + /// In en, this message translates to: + /// **'Multi-ACKs: {value}'** + String settings_multiAck(String value); + + /// No description provided for @settings_telemetryModeUpdated. + /// + /// In en, this message translates to: + /// **'Telemetry mode updated'** + String get settings_telemetryModeUpdated; + /// No description provided for @settings_actions. /// /// In en, this message translates to: @@ -1846,6 +1924,72 @@ abstract class AppLocalizations { /// **'~ {days} days'** String contacts_lastSeenDaysAgo(int days); + /// No description provided for @contact_info. + /// + /// In en, this message translates to: + /// **'Contact Info'** + String get contact_info; + + /// No description provided for @contact_settings. + /// + /// In en, this message translates to: + /// **'Contact Settings'** + String get contact_settings; + + /// No description provided for @contact_telemetry. + /// + /// In en, this message translates to: + /// **'Telemetry'** + String get contact_telemetry; + + /// No description provided for @contact_lastSeen. + /// + /// In en, this message translates to: + /// **'Last seen'** + String get contact_lastSeen; + + /// No description provided for @contact_clearChat. + /// + /// In en, this message translates to: + /// **'Clear Chat'** + String get contact_clearChat; + + /// No description provided for @contact_teleBase. + /// + /// In en, this message translates to: + /// **'Telemetry Base'** + String get contact_teleBase; + + /// No description provided for @contact_teleBaseSubtitle. + /// + /// In en, this message translates to: + /// **'Allow sharing battery level and basic telemetry'** + String get contact_teleBaseSubtitle; + + /// No description provided for @contact_teleLoc. + /// + /// In en, this message translates to: + /// **'Telemetry Location'** + String get contact_teleLoc; + + /// No description provided for @contact_teleLocSubtitle. + /// + /// In en, this message translates to: + /// **'Allow sharing location data'** + String get contact_teleLocSubtitle; + + /// No description provided for @contact_teleEnv. + /// + /// In en, this message translates to: + /// **'Telemetry Environment'** + String get contact_teleEnv; + + /// No description provided for @contact_teleEnvSubtitle. + /// + /// In en, this message translates to: + /// **'Allow sharing environment sensor data'** + String get contact_teleEnvSubtitle; + /// No description provided for @channels_title. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 2821617..d6537f9 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -398,6 +398,52 @@ class AppLocalizationsBg extends AppLocalizations { String get settings_privacyModeDisabled => 'Режим на поверителност е деактивиран'; + @override + String get settings_privacy => 'Настройки на поверителността'; + + @override + String get settings_privacySubtitle => + 'Контролирайте каква информация се споделя.'; + + @override + String get settings_privacySettingsDescription => + 'Изберете каква информация устройството ви споделя с другите.'; + + @override + String get settings_denyAll => 'Откажи всичко'; + + @override + String get settings_allowByContact => 'Позволи по флагове за контакт'; + + @override + String get settings_allowAll => 'Позволи всичко'; + + @override + String get settings_telemetryBaseMode => 'Базов режим на телеметрия'; + + @override + String get settings_telemetryLocationMode => + 'Режим на местоположение на телеметрията'; + + @override + String get settings_telemetryEnvironmentMode => + 'Режим на средата на телеметрията'; + + @override + String get settings_advertLocation => 'Място на обявата'; + + @override + String get settings_advertLocationSubtitle => + 'Включи местоположение в обявата'; + + @override + String settings_multiAck(String value) { + return 'Мулти-потвърди: $value'; + } + + @override + String get settings_telemetryModeUpdated => 'Режим на телеметрията е обновен'; + @override String get settings_actions => 'Действия'; @@ -989,6 +1035,42 @@ class AppLocalizationsBg extends AppLocalizations { return 'Последно видян $days дни преди.'; } + @override + String get contact_info => 'Контактна информация'; + + @override + String get contact_settings => 'Настройки за контакти'; + + @override + String get contact_telemetry => 'Телеметрия'; + + @override + String get contact_lastSeen => 'Последно видян'; + + @override + String get contact_clearChat => 'Изчисти чата'; + + @override + String get contact_teleBase => 'Базата данни за телеметрия'; + + @override + String get contact_teleBaseSubtitle => + 'Позволи споделяне на ниво на батерията и основна телеметрия'; + + @override + String get contact_teleLoc => 'Местоположение на телеметрията'; + + @override + String get contact_teleLocSubtitle => + 'Позволи споделяне на данни за местоположение'; + + @override + String get contact_teleEnv => 'Среда на телеметрия'; + + @override + String get contact_teleEnvSubtitle => + 'Позволи споделяне на данни от средносферните датчици'; + @override String get channels_title => 'Канали'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 337915e..87fab6f 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -398,6 +398,50 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_privacyModeDisabled => 'Datenschutzmodus deaktiviert'; + @override + String get settings_privacy => 'Datenschutzeinstellungen'; + + @override + String get settings_privacySubtitle => + 'Steuern Sie die Informationen, die freigegeben werden.'; + + @override + String get settings_privacySettingsDescription => + 'Wählen Sie die Informationen, die Ihr Gerät mit anderen teilt.'; + + @override + String get settings_denyAll => 'Alle ablehnen'; + + @override + String get settings_allowByContact => 'Zulassen durch Kontaktflaggen'; + + @override + String get settings_allowAll => 'Alles zulassen'; + + @override + String get settings_telemetryBaseMode => 'Telemetrie-Basismodus'; + + @override + String get settings_telemetryLocationMode => 'Telemetrie-Ortsmodus'; + + @override + String get settings_telemetryEnvironmentMode => 'Telemetrie-Umgebungsmodus'; + + @override + String get settings_advertLocation => 'Anzeigenort'; + + @override + String get settings_advertLocationSubtitle => + 'Ort in der Anzeige einbeziehen'; + + @override + String settings_multiAck(String value) { + return 'Mehrfach-Bestätigungen: $value'; + } + + @override + String get settings_telemetryModeUpdated => 'Telemetriemodus aktualisiert'; + @override String get settings_actions => 'Aktionen'; @@ -987,6 +1031,41 @@ class AppLocalizationsDe extends AppLocalizations { return '~ $days Tage'; } + @override + String get contact_info => 'Kontaktinformationen'; + + @override + String get contact_settings => 'Kontakteinstellungen'; + + @override + String get contact_telemetry => 'Telemetrie'; + + @override + String get contact_lastSeen => 'Zuletzt gesehen'; + + @override + String get contact_clearChat => 'Chat löschen'; + + @override + String get contact_teleBase => 'Telemetriebasis'; + + @override + String get contact_teleBaseSubtitle => + 'Erlauben des Freigebens des Batteriestands und der grundlegenden Telemetrie'; + + @override + String get contact_teleLoc => 'Telemetrieort'; + + @override + String get contact_teleLocSubtitle => 'Teilen von Standortdaten zulassen'; + + @override + String get contact_teleEnv => 'Telemetrieumgebung'; + + @override + String get contact_teleEnvSubtitle => + 'Teilen von Umgebungsensordaten zulassen'; + @override String get channels_title => 'Kanäle'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 1e4e5b0..1e0196b 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -392,6 +392,48 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settings_privacyModeDisabled => 'Privacy mode disabled'; + @override + String get settings_privacy => 'Privacy Settings'; + + @override + String get settings_privacySubtitle => 'Control what information is shared.'; + + @override + String get settings_privacySettingsDescription => + 'Choose what information your device shares with others.'; + + @override + String get settings_denyAll => 'Deny all'; + + @override + String get settings_allowByContact => 'Allow by contact flags'; + + @override + String get settings_allowAll => 'Allow all'; + + @override + String get settings_telemetryBaseMode => 'Telemetry Base Mode'; + + @override + String get settings_telemetryLocationMode => 'Telemetry Location Mode'; + + @override + String get settings_telemetryEnvironmentMode => 'Telemetry Environment Mode'; + + @override + String get settings_advertLocation => 'Advert Location'; + + @override + String get settings_advertLocationSubtitle => 'Include location in advert.'; + + @override + String settings_multiAck(String value) { + return 'Multi-ACKs: $value'; + } + + @override + String get settings_telemetryModeUpdated => 'Telemetry mode updated'; + @override String get settings_actions => 'Actions'; @@ -972,6 +1014,40 @@ class AppLocalizationsEn extends AppLocalizations { return '~ $days days'; } + @override + String get contact_info => 'Contact Info'; + + @override + String get contact_settings => 'Contact Settings'; + + @override + String get contact_telemetry => 'Telemetry'; + + @override + String get contact_lastSeen => 'Last seen'; + + @override + String get contact_clearChat => 'Clear Chat'; + + @override + String get contact_teleBase => 'Telemetry Base'; + + @override + String get contact_teleBaseSubtitle => + 'Allow sharing battery level and basic telemetry'; + + @override + String get contact_teleLoc => 'Telemetry Location'; + + @override + String get contact_teleLocSubtitle => 'Allow sharing location data'; + + @override + String get contact_teleEnv => 'Telemetry Environment'; + + @override + String get contact_teleEnvSubtitle => 'Allow sharing environment sensor data'; + @override String get channels_title => 'Channels'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 657d556..dff2e5e 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -396,6 +396,51 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_privacyModeDisabled => 'Modo de privacidad desactivado'; + @override + String get settings_privacy => 'Configuración de privacidad'; + + @override + String get settings_privacySubtitle => + 'Controlar qué información se comparte.'; + + @override + String get settings_privacySettingsDescription => + 'Elige qué información comparte tu dispositivo con otros.'; + + @override + String get settings_denyAll => 'Denegar todo'; + + @override + String get settings_allowByContact => 'Permitir por banderas de contacto'; + + @override + String get settings_allowAll => 'Permitir todo'; + + @override + String get settings_telemetryBaseMode => 'Modo base de telemetría'; + + @override + String get settings_telemetryLocationMode => + 'Modo de ubicación de telemetría'; + + @override + String get settings_telemetryEnvironmentMode => + 'Modo de entorno de telemetría'; + + @override + String get settings_advertLocation => 'Ubicación de anuncio'; + + @override + String get settings_advertLocationSubtitle => 'Incluir ubicación en anuncio'; + + @override + String settings_multiAck(String value) { + return 'Multi-ACKs: $value'; + } + + @override + String get settings_telemetryModeUpdated => 'Modo de telemetría actualizado'; + @override String get settings_actions => 'Acciones'; @@ -987,6 +1032,42 @@ class AppLocalizationsEs extends AppLocalizations { return '~ $days días'; } + @override + String get contact_info => 'Información de contacto'; + + @override + String get contact_settings => 'Configuración de contacto'; + + @override + String get contact_telemetry => 'Telemetría'; + + @override + String get contact_lastSeen => 'Visto por última vez'; + + @override + String get contact_clearChat => 'Borrar chat'; + + @override + String get contact_teleBase => 'Base de Telemetría'; + + @override + String get contact_teleBaseSubtitle => + 'Permitir el intercambio de nivel de batería y telemetría básica'; + + @override + String get contact_teleLoc => 'Ubicación de telemetría'; + + @override + String get contact_teleLocSubtitle => + 'Permitir el intercambio de datos de ubicación'; + + @override + String get contact_teleEnv => 'Entorno de Telemetría'; + + @override + String get contact_teleEnvSubtitle => + 'Permitir el intercambio de datos de sensores de entorno'; + @override String get channels_title => 'Canales'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 7aa7ebe..91bf4f4 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -400,6 +400,52 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_privacyModeDisabled => 'Mode de confidentialité désactivé'; + @override + String get settings_privacy => 'Paramètres de confidentialité'; + + @override + String get settings_privacySubtitle => 'Contrôlez les informations partagées'; + + @override + String get settings_privacySettingsDescription => + 'Choisissez les informations que votre appareil partage avec les autres.'; + + @override + String get settings_denyAll => 'Refuser tout'; + + @override + String get settings_allowByContact => 'Autoriser par drapeaux de contact'; + + @override + String get settings_allowAll => 'Autoriser tout'; + + @override + String get settings_telemetryBaseMode => 'Mode de base Télémétrie'; + + @override + String get settings_telemetryLocationMode => + 'Mode d\'emplacement de télémétrie'; + + @override + String get settings_telemetryEnvironmentMode => + 'Mode d\'environnement de télémétrie'; + + @override + String get settings_advertLocation => 'Emplacement de l\'annonce'; + + @override + String get settings_advertLocationSubtitle => + 'Inclure l\'emplacement dans l\'annonce'; + + @override + String settings_multiAck(String value) { + return 'Multi-ACKs : $value'; + } + + @override + String get settings_telemetryModeUpdated => + 'Le mode télémétrie a été mis à jour'; + @override String get settings_actions => 'Actions'; @@ -991,6 +1037,42 @@ class AppLocalizationsFr extends AppLocalizations { return '~ $days jours'; } + @override + String get contact_info => 'Informations de contact'; + + @override + String get contact_settings => 'Paramètres de contact'; + + @override + String get contact_telemetry => 'Télémétrie'; + + @override + String get contact_lastSeen => 'Dernière fois vu'; + + @override + String get contact_clearChat => 'Effacer la conversation'; + + @override + String get contact_teleBase => 'Base de télémétrie'; + + @override + String get contact_teleBaseSubtitle => + 'Autoriser le partage du niveau de batterie et de la télémétrie de base'; + + @override + String get contact_teleLoc => 'Emplacement de télémétrie'; + + @override + String get contact_teleLocSubtitle => + 'Autoriser le partage des données de localisation'; + + @override + String get contact_teleEnv => 'Environnement Télémétrie'; + + @override + String get contact_teleEnvSubtitle => + 'Autoriser le partage des données des capteurs d\'environnement'; + @override String get channels_title => 'Canaux'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 02c5937..b688c06 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -398,6 +398,52 @@ class AppLocalizationsIt extends AppLocalizations { @override String get settings_privacyModeDisabled => 'Modalità privacy disabilitata'; + @override + String get settings_privacy => 'Impostazioni sulla privacy'; + + @override + String get settings_privacySubtitle => + 'Controlla le informazioni che vengono condivise.'; + + @override + String get settings_privacySettingsDescription => + 'Scegli le informazioni che il tuo dispositivo condivide con gli altri.'; + + @override + String get settings_denyAll => 'Negare tutto'; + + @override + String get settings_allowByContact => 'Consenti in base ai flag di contatto'; + + @override + String get settings_allowAll => 'Consenti tutto'; + + @override + String get settings_telemetryBaseMode => 'Modalità di base di telemetria'; + + @override + String get settings_telemetryLocationMode => + 'Modalità di posizionamento telemetrico'; + + @override + String get settings_telemetryEnvironmentMode => + 'Modalità di ambiente di telemetria'; + + @override + String get settings_advertLocation => 'Posizione dell\'annuncio'; + + @override + String get settings_advertLocationSubtitle => + 'Includi la posizione nell\'annuncio'; + + @override + String settings_multiAck(String value) { + return 'Multi-ACKs: $value'; + } + + @override + String get settings_telemetryModeUpdated => 'Modalità telemetria aggiornata'; + @override String get settings_actions => 'Azioni'; @@ -987,6 +1033,42 @@ class AppLocalizationsIt extends AppLocalizations { return 'Ultimo visto $days giorni fa'; } + @override + String get contact_info => 'Informazioni di Contatto'; + + @override + String get contact_settings => 'Impostazioni di contatto'; + + @override + String get contact_telemetry => 'Telemetria'; + + @override + String get contact_lastSeen => 'Ultimo accesso'; + + @override + String get contact_clearChat => 'Cancella chat'; + + @override + String get contact_teleBase => 'Base di telemetria'; + + @override + String get contact_teleBaseSubtitle => + 'Consenti la condivisione del livello della batteria e della telemetria di base'; + + @override + String get contact_teleLoc => 'Posizione telemetria'; + + @override + String get contact_teleLocSubtitle => + 'Consenti la condivisione dei dati di posizione'; + + @override + String get contact_teleEnv => 'Ambiente di telemetria'; + + @override + String get contact_teleEnvSubtitle => + 'Consenti la condivisione dei dati del sensore ambientale'; + @override String get channels_title => 'Canali'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 9e51164..1530886 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -395,6 +395,50 @@ class AppLocalizationsNl extends AppLocalizations { @override String get settings_privacyModeDisabled => 'Privacy modus is uitgeschakeld'; + @override + String get settings_privacy => 'Privacyinstellingen'; + + @override + String get settings_privacySubtitle => + 'Beheer welke informatie wordt gedeeld'; + + @override + String get settings_privacySettingsDescription => + 'Kies welke informatie uw apparaat deelt met anderen'; + + @override + String get settings_denyAll => 'Weiger alles'; + + @override + String get settings_allowByContact => 'Toestaan op basis van contactvlaggen'; + + @override + String get settings_allowAll => 'Alles toestaan'; + + @override + String get settings_telemetryBaseMode => 'Telemetrie-basismodus'; + + @override + String get settings_telemetryLocationMode => 'Telemetrie-locatiemodus'; + + @override + String get settings_telemetryEnvironmentMode => 'Telemetrie-omgevingsmodus'; + + @override + String get settings_advertLocation => 'Advertentielocatie'; + + @override + String get settings_advertLocationSubtitle => + 'Locatie opnemen in advertentie'; + + @override + String settings_multiAck(String value) { + return 'Multi-ACKs: $value'; + } + + @override + String get settings_telemetryModeUpdated => 'Telemetrie-modus bijgewerkt'; + @override String get settings_actions => 'Acties'; @@ -980,6 +1024,40 @@ class AppLocalizationsNl extends AppLocalizations { return 'Laast gezien $days dagen geleden'; } + @override + String get contact_info => 'Contactinformatie'; + + @override + String get contact_settings => 'Contactinstellingen'; + + @override + String get contact_telemetry => 'Telemetrie'; + + @override + String get contact_lastSeen => 'Laatst gezien'; + + @override + String get contact_clearChat => 'Chat leegmaken'; + + @override + String get contact_teleBase => 'Telemetrie_basis'; + + @override + String get contact_teleBaseSubtitle => + 'Sta delen van batterij niveau en basis telemetrie toe'; + + @override + String get contact_teleLoc => 'Telemetrielocatie'; + + @override + String get contact_teleLocSubtitle => 'Locatiegegevens delen toestaan'; + + @override + String get contact_teleEnv => 'Telemetrieomgeving'; + + @override + String get contact_teleEnvSubtitle => 'Delen van omgevingsensordata toestaan'; + @override String get channels_title => 'Kanaal'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 6fb41dc..6938858 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -401,6 +401,52 @@ class AppLocalizationsPl extends AppLocalizations { @override String get settings_privacyModeDisabled => 'Tryb prywatności wyłączony'; + @override + String get settings_privacy => 'Ustawienia prywatności'; + + @override + String get settings_privacySubtitle => + 'Kontroluj jakie informacje są udostępniane.'; + + @override + String get settings_privacySettingsDescription => + 'Wybierz jakie informacje urządzenie udostępni innym.'; + + @override + String get settings_denyAll => 'Odmów wszystkim'; + + @override + String get settings_allowByContact => 'Zezwalaj według flag kontaktowych'; + + @override + String get settings_allowAll => 'Zezwalaj na wszystko'; + + @override + String get settings_telemetryBaseMode => 'Tryb podstawowy telemetrii'; + + @override + String get settings_telemetryLocationMode => 'Tryb położenia telemetrycznego'; + + @override + String get settings_telemetryEnvironmentMode => + 'Tryb środowiska telemetrycznego'; + + @override + String get settings_advertLocation => 'Lokalizacja reklamowa'; + + @override + String get settings_advertLocationSubtitle => + 'Uwzględnij lokalizację w ogłoszeniu'; + + @override + String settings_multiAck(String value) { + return 'Wiele potwierdzeń: $value'; + } + + @override + String get settings_telemetryModeUpdated => + 'Tryb telemetryczny zaktualizowany'; + @override String get settings_actions => 'Działania'; @@ -996,6 +1042,42 @@ class AppLocalizationsPl extends AppLocalizations { return '~ $days dni'; } + @override + String get contact_info => 'Informacje kontaktowe'; + + @override + String get contact_settings => 'Ustawienia kontaktowe'; + + @override + String get contact_telemetry => 'Telemetryka'; + + @override + String get contact_lastSeen => 'Ostatnio widziany'; + + @override + String get contact_clearChat => 'Wyczyść czat'; + + @override + String get contact_teleBase => 'Baza telemetryczna'; + + @override + String get contact_teleBaseSubtitle => + 'Pozwól na udostępnianie poziomu naładowania baterii i podstawowych danych telemetrycznych'; + + @override + String get contact_teleLoc => 'Lokalizacja telemetryczna'; + + @override + String get contact_teleLocSubtitle => + 'Zezwalaj na udostępnianie danych lokalizacji'; + + @override + String get contact_teleEnv => 'Środowisko telemetryczne'; + + @override + String get contact_teleEnvSubtitle => + 'Zezwalaj na udostępnianie danych czujników środowiskowych'; + @override String get channels_title => 'Kanały'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index a51e1b0..87b44ca 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -398,6 +398,51 @@ class AppLocalizationsPt extends AppLocalizations { @override String get settings_privacyModeDisabled => 'Modo de privacidade desativado'; + @override + String get settings_privacy => 'Configurações de Privacidade'; + + @override + String get settings_privacySubtitle => 'Controle o que é compartilhado.'; + + @override + String get settings_privacySettingsDescription => + 'Escolha quais informações o seu dispositivo compartilha com os outros.'; + + @override + String get settings_denyAll => 'Negar todos'; + + @override + String get settings_allowByContact => 'Permitir por bandeiras de contato'; + + @override + String get settings_allowAll => 'Permitir todos'; + + @override + String get settings_telemetryBaseMode => 'Modo Base de Telemetria'; + + @override + String get settings_telemetryLocationMode => + 'Modo de Localização de Telemetria'; + + @override + String get settings_telemetryEnvironmentMode => + 'Modo de Ambiente de Telemetria'; + + @override + String get settings_advertLocation => 'Localização do Anúncio'; + + @override + String get settings_advertLocationSubtitle => + 'Incluir localização no anúncio'; + + @override + String settings_multiAck(String value) { + return 'Multi-ACKs: $value'; + } + + @override + String get settings_telemetryModeUpdated => 'Modo de telemetria atualizado'; + @override String get settings_actions => 'Ações'; @@ -988,6 +1033,42 @@ class AppLocalizationsPt extends AppLocalizations { return 'Última vez visto $days dias atrás'; } + @override + String get contact_info => 'Informações de Contato'; + + @override + String get contact_settings => 'Configurações de Contato'; + + @override + String get contact_telemetry => 'Telemetria'; + + @override + String get contact_lastSeen => 'Visto pela última vez'; + + @override + String get contact_clearChat => 'Limpar Chat'; + + @override + String get contact_teleBase => 'Base de Telemetria'; + + @override + String get contact_teleBaseSubtitle => + 'Permitir compartilhamento do nível da bateria e telemetria básica'; + + @override + String get contact_teleLoc => 'Localização de Telemetria'; + + @override + String get contact_teleLocSubtitle => + 'Permitir compartilhamento de dados de localização'; + + @override + String get contact_teleEnv => 'Ambiente de Telemetria'; + + @override + String get contact_teleEnvSubtitle => + 'Permitir compartilhamento de dados do sensor de ambiente'; + @override String get channels_title => 'Canais'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 7a6998f..72d2e1c 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -398,6 +398,51 @@ class AppLocalizationsRu extends AppLocalizations { String get settings_privacyModeDisabled => 'Режим конфиденциальности выключен'; + @override + String get settings_privacy => 'Настройки конфиденциальности'; + + @override + String get settings_privacySubtitle => + 'Контролируйте, какую информацию делиться.'; + + @override + String get settings_privacySettingsDescription => + 'Выберите, какую информацию ваше устройство будет делиться с другими.'; + + @override + String get settings_denyAll => 'Отклонить все'; + + @override + String get settings_allowByContact => 'Разрешить по флагам контактов'; + + @override + String get settings_allowAll => 'Разрешить все'; + + @override + String get settings_telemetryBaseMode => 'Базовый режим телеметрии'; + + @override + String get settings_telemetryLocationMode => + 'Режим местоположения телеметрии'; + + @override + String get settings_telemetryEnvironmentMode => 'Режим среды телеметрии'; + + @override + String get settings_advertLocation => 'Местоположение рекламы'; + + @override + String get settings_advertLocationSubtitle => + 'Включить местоположение в объявление'; + + @override + String settings_multiAck(String value) { + return 'Мульти-ACK: $value'; + } + + @override + String get settings_telemetryModeUpdated => 'Режим телеметрии обновлен'; + @override String get settings_actions => 'Действия'; @@ -988,6 +1033,42 @@ class AppLocalizationsRu extends AppLocalizations { return 'Видели $days дн. назад'; } + @override + String get contact_info => 'Контактная информация'; + + @override + String get contact_settings => 'Настройки контактов'; + + @override + String get contact_telemetry => 'Телеметрия'; + + @override + String get contact_lastSeen => 'Последний раз видели'; + + @override + String get contact_clearChat => 'Очистить чат'; + + @override + String get contact_teleBase => 'База телеметрии'; + + @override + String get contact_teleBaseSubtitle => + 'Разрешить обмен уровнем заряда батареи и базовой телеметрией'; + + @override + String get contact_teleLoc => 'Местоположение телеметрии'; + + @override + String get contact_teleLocSubtitle => + 'Разрешить обмен данными о местоположении'; + + @override + String get contact_teleEnv => 'Среда телеметрии'; + + @override + String get contact_teleEnvSubtitle => + 'Разрешить обмен данными датчиков окружающей среды'; + @override String get channels_title => 'Каналы'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index ae6c956..7817af6 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -395,6 +395,49 @@ class AppLocalizationsSk extends AppLocalizations { @override String get settings_privacyModeDisabled => 'Ochranný režim je vypnutý'; + @override + String get settings_privacy => 'Nastavenia súkromia'; + + @override + String get settings_privacySubtitle => 'Ovládni, aké informácie sa zdieľajú.'; + + @override + String get settings_privacySettingsDescription => + 'Vyberte, ktoré informácie váš zariadenie zdieľa s ostatnými.'; + + @override + String get settings_denyAll => 'Zamietnuť všetko'; + + @override + String get settings_allowByContact => 'Povoliť podľa kontaktových vlajok'; + + @override + String get settings_allowAll => 'Povoliť všetko'; + + @override + String get settings_telemetryBaseMode => 'Základný režim telemetrie'; + + @override + String get settings_telemetryLocationMode => 'Režim umiestnenia telemetrie'; + + @override + String get settings_telemetryEnvironmentMode => 'Režim prostredia telemetrie'; + + @override + String get settings_advertLocation => 'Umiestnenie inzerátu'; + + @override + String get settings_advertLocationSubtitle => 'Zahrnúť polohu do inzerátu'; + + @override + String settings_multiAck(String value) { + return 'Viaceré ACK: $value'; + } + + @override + String get settings_telemetryModeUpdated => + 'Režim telemetrie bol aktualizovaný'; + @override String get settings_actions => 'Možné akcie'; @@ -980,6 +1023,41 @@ class AppLocalizationsSk extends AppLocalizations { return 'Posledné zobrazenie $days dní dozadu'; } + @override + String get contact_info => 'Kontaktné informácie'; + + @override + String get contact_settings => 'Nastavenia kontaktov'; + + @override + String get contact_telemetry => 'Telemetria'; + + @override + String get contact_lastSeen => 'Naposledy videný'; + + @override + String get contact_clearChat => 'Vymazať chat'; + + @override + String get contact_teleBase => 'Báza telemetrie'; + + @override + String get contact_teleBaseSubtitle => + 'Povoliť zdieľanie úrovne batérie a základnej telemetrie'; + + @override + String get contact_teleLoc => 'Lokácia telemetrie'; + + @override + String get contact_teleLocSubtitle => 'Povoliť zdieľanie údajov o lokalite'; + + @override + String get contact_teleEnv => 'Prostredie telemetrie'; + + @override + String get contact_teleEnvSubtitle => + 'Povoliť zdieľanie údajov senzorov prostredia'; + @override String get channels_title => 'Kanály'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 96501cd..6032ee0 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -393,6 +393,50 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_privacyModeDisabled => 'Privatni način je onemogočen.'; + @override + String get settings_privacy => 'Nastavitve zasebnosti'; + + @override + String get settings_privacySubtitle => + 'Kontrolirajte, katere informacije so deljene.'; + + @override + String get settings_privacySettingsDescription => + 'Izberite, katere informacije vaš naprava deli z drugimi.'; + + @override + String get settings_denyAll => 'Zavrniti vse'; + + @override + String get settings_allowByContact => 'Dovoli po kontaktnih zastavah'; + + @override + String get settings_allowAll => 'Dovoli vse'; + + @override + String get settings_telemetryBaseMode => 'Osnovni način telemetrije'; + + @override + String get settings_telemetryLocationMode => 'Način delovanja telemetrije'; + + @override + String get settings_telemetryEnvironmentMode => + 'Način delovanja okolja telemetrije'; + + @override + String get settings_advertLocation => 'Lokacija oglasa'; + + @override + String get settings_advertLocationSubtitle => 'Vključi lokacijo v oglas.'; + + @override + String settings_multiAck(String value) { + return 'Večkratni potrditvi: $value'; + } + + @override + String get settings_telemetryModeUpdated => 'Način telemetrije posodobljen'; + @override String get settings_actions => 'Akcije'; @@ -977,6 +1021,41 @@ class AppLocalizationsSl extends AppLocalizations { return 'Zadnjič viden pred $days dnem'; } + @override + String get contact_info => 'Kontaktni podatki'; + + @override + String get contact_settings => 'Nastavitve stika'; + + @override + String get contact_telemetry => 'Telemetrija'; + + @override + String get contact_lastSeen => 'Zadnjič videno'; + + @override + String get contact_clearChat => 'Počisti klepet'; + + @override + String get contact_teleBase => 'Baza telemetrije'; + + @override + String get contact_teleBaseSubtitle => + 'Dovoli deljenje stanja baterije in osnovne telemetrije'; + + @override + String get contact_teleLoc => 'Lokacija telemetrije'; + + @override + String get contact_teleLocSubtitle => 'Dovoli deljenje podatkov o lokaciji'; + + @override + String get contact_teleEnv => 'Okolje telemetrije'; + + @override + String get contact_teleEnvSubtitle => + 'Dovoli deljenje podatkov okoljskih senzorjev'; + @override String get channels_title => 'Kanali'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index a834230..5b19be3 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -392,6 +392,49 @@ class AppLocalizationsSv extends AppLocalizations { @override String get settings_privacyModeDisabled => 'Privatläge är avstängt'; + @override + String get settings_privacy => 'Inställningar för sekretess'; + + @override + String get settings_privacySubtitle => + 'Kontrollera vilken information som delas.'; + + @override + String get settings_privacySettingsDescription => + 'Välj vilken information din enhet delar med andra.'; + + @override + String get settings_denyAll => 'Neka alla'; + + @override + String get settings_allowByContact => 'Tillåt via kontaktflaggor'; + + @override + String get settings_allowAll => 'Tillåt alla'; + + @override + String get settings_telemetryBaseMode => 'Telemetribasläge'; + + @override + String get settings_telemetryLocationMode => 'Telemetritillstånd för plats'; + + @override + String get settings_telemetryEnvironmentMode => 'Telemetri miljöläge'; + + @override + String get settings_advertLocation => 'Annonsplacering'; + + @override + String get settings_advertLocationSubtitle => 'Inkludera plats i annonsen'; + + @override + String settings_multiAck(String value) { + return 'Multi-ACKs: $value'; + } + + @override + String get settings_telemetryModeUpdated => 'Telemetri-läge uppdaterat'; + @override String get settings_actions => 'Åtgärder'; @@ -972,6 +1015,40 @@ class AppLocalizationsSv extends AppLocalizations { return 'Senast synlig $days dagar sedan'; } + @override + String get contact_info => 'Kontaktinformation'; + + @override + String get contact_settings => 'Kontaktinställningar'; + + @override + String get contact_telemetry => 'Telemetri'; + + @override + String get contact_lastSeen => 'Senast sedd'; + + @override + String get contact_clearChat => 'Rensa Chatt'; + + @override + String get contact_teleBase => 'Telemetribas'; + + @override + String get contact_teleBaseSubtitle => + 'Tillåt delning av batterinivå och grundläggande telemetri'; + + @override + String get contact_teleLoc => 'Telemetridata plats'; + + @override + String get contact_teleLocSubtitle => 'Tillåt delning av platsdata'; + + @override + String get contact_teleEnv => 'Telemetri Miljö'; + + @override + String get contact_teleEnvSubtitle => 'Tillåt delning av miljösensordata'; + @override String get channels_title => 'Kanaler'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 7db1cc7..096e470 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -395,6 +395,50 @@ class AppLocalizationsUk extends AppLocalizations { @override String get settings_privacyModeDisabled => 'Режим приватності вимкнено'; + @override + String get settings_privacy => 'Налаштування приватності'; + + @override + String get settings_privacySubtitle => + 'Керуйте інформацією, яку буде спільно використовуватися'; + + @override + String get settings_privacySettingsDescription => + 'Виберіть, яку інформацію ваш пристрій буде передавати іншим.'; + + @override + String get settings_denyAll => 'Відхилити все'; + + @override + String get settings_allowByContact => 'Дозволити за контактними прапорцями'; + + @override + String get settings_allowAll => 'Дозволити все'; + + @override + String get settings_telemetryBaseMode => 'Режим базової телеметрії'; + + @override + String get settings_telemetryLocationMode => 'Режим місця телеметрії'; + + @override + String get settings_telemetryEnvironmentMode => 'Режим середовища телеметрії'; + + @override + String get settings_advertLocation => 'Розміщення реклами'; + + @override + String get settings_advertLocationSubtitle => + 'Включити місце розташування в оголошення'; + + @override + String settings_multiAck(String value) { + return 'Багатократне підтвердження: $value'; + } + + @override + String get settings_telemetryModeUpdated => 'Режим телеметрії оновлено'; + @override String get settings_actions => 'Дії'; @@ -983,6 +1027,42 @@ class AppLocalizationsUk extends AppLocalizations { return 'В мережі $days дн. тому'; } + @override + String get contact_info => 'Контактна інформація'; + + @override + String get contact_settings => 'Налаштування контактів'; + + @override + String get contact_telemetry => 'Телеметрія'; + + @override + String get contact_lastSeen => 'Останній раз бачили'; + + @override + String get contact_clearChat => 'Очистити чат'; + + @override + String get contact_teleBase => 'Базовий телебачення'; + + @override + String get contact_teleBaseSubtitle => + 'Дозволити спільний доступ до рівня заряду батареї та базової телеметрії'; + + @override + String get contact_teleLoc => 'Розташування телеметрії'; + + @override + String get contact_teleLocSubtitle => + 'Дозволити спільне використання даних про місцеположення'; + + @override + String get contact_teleEnv => 'Середовище телеметрії'; + + @override + String get contact_teleEnvSubtitle => + 'Дозволити спільний доступ до даних датчиків середовища'; + @override String get channels_title => 'Канали'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index dc1a17e..f142763 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -374,6 +374,47 @@ class AppLocalizationsZh extends AppLocalizations { @override String get settings_privacyModeDisabled => '隐私模式已关闭'; + @override + String get settings_privacy => '隐私设置'; + + @override + String get settings_privacySubtitle => '控制要共享的信息。'; + + @override + String get settings_privacySettingsDescription => '选择您的设备与他人共享的信息。'; + + @override + String get settings_denyAll => '拒绝所有'; + + @override + String get settings_allowByContact => '按联系人标志允许'; + + @override + String get settings_allowAll => '允许全部'; + + @override + String get settings_telemetryBaseMode => '遥测基础模式'; + + @override + String get settings_telemetryLocationMode => '遥测位置模式'; + + @override + String get settings_telemetryEnvironmentMode => '遥测环境模式'; + + @override + String get settings_advertLocation => '广告位置'; + + @override + String get settings_advertLocationSubtitle => '在广告中包含位置'; + + @override + String settings_multiAck(String value) { + return '多重ACK:$value'; + } + + @override + String get settings_telemetryModeUpdated => '遥测模式已更新'; + @override String get settings_actions => '操作'; @@ -923,6 +964,39 @@ class AppLocalizationsZh extends AppLocalizations { return '最后在线 $days 天前'; } + @override + String get contact_info => '联系信息'; + + @override + String get contact_settings => '联系人设置'; + + @override + String get contact_telemetry => '遥测数据'; + + @override + String get contact_lastSeen => '最近出现'; + + @override + String get contact_clearChat => '清除聊天记录'; + + @override + String get contact_teleBase => '遥测基站'; + + @override + String get contact_teleBaseSubtitle => '允许共享电池电量和基本遥测数据'; + + @override + String get contact_teleLoc => '遥测位置'; + + @override + String get contact_teleLocSubtitle => '允许共享位置数据'; + + @override + String get contact_teleEnv => '遥测环境'; + + @override + String get contact_teleEnvSubtitle => '允许共享环境传感器数据'; + @override String get channels_title => '频道'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 3caea31..1b5e78c 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1900,6 +1900,35 @@ } } }, + "settings_privacy": "Privacyinstellingen", + "settings_privacySubtitle": "Beheer welke informatie wordt gedeeld", + "settings_telemetryLocationMode": "Telemetrie-locatiemodus", + "settings_telemetryEnvironmentMode": "Telemetrie-omgevingsmodus", + "settings_advertLocation": "Advertentielocatie", + "settings_advertLocationSubtitle": "Locatie opnemen in advertentie", + "settings_privacySettingsDescription": "Kies welke informatie uw apparaat deelt met anderen", + "settings_allowByContact": "Toestaan op basis van contactvlaggen", + "settings_allowAll": "Alles toestaan", + "settings_denyAll": "Weiger alles", + "contact_info": "Contactinformatie", + "settings_telemetryBaseMode": "Telemetrie-basismodus", + "contact_teleBase": "Telemetrie_basis", + "contact_teleLoc": "Telemetrielocatie", + "contact_teleLocSubtitle": "Locatiegegevens delen toestaan", + "contact_teleEnv": "Telemetrieomgeving", + "contact_teleEnvSubtitle": "Delen van omgevingsensordata toestaan", + "contact_settings": "Contactinstellingen", + "contact_telemetry": "Telemetrie", + "contact_lastSeen": "Laatst gezien", + "contact_clearChat": "Chat leegmaken", + "contact_teleBaseSubtitle": "Sta delen van batterij niveau en basis telemetrie toe", + "@settings_multiAck": { + "placeholders": { + "value": { + "type": "String" + } + } + }, "appSettings_maxRouteWeightSubtitle": "Het maximale gewicht dat een route kan bereiken door succesvolle leveringen.", "appSettings_initialRouteWeight": "เริ่มต้น gewicht van de route", "appSettings_maxRouteWeight": "Maximale gewicht voor de route", @@ -1910,5 +1939,7 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Gewicht verwijderd van een pad na een mislukte levering", "appSettings_maxMessageRetries": "Aantal pogingen om berichten te versturen", "appSettings_maxMessageRetriesSubtitle": "Aantal pogingen om een bericht opnieuw te versturen voordat het als mislukt wordt gemarkeerd", - "path_routeWeight": "{weight}/{max}" -} + "path_routeWeight": "{weight}/{max}", + "settings_telemetryModeUpdated": "Telemetrie-modus bijgewerkt", + "settings_multiAck": "Multi-ACKs: {value}" +} \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 9cf0afc..1ccad59 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1938,15 +1938,46 @@ } } }, - "appSettings_initialRouteWeight": "Początkowa waga ścieżki", - "appSettings_maxRouteWeight": "Maksymalna waga ścieżki", - "appSettings_initialRouteWeightSubtitle": "Waga początkowa dla nowo odkrytych ścieżek", - "appSettings_maxRouteWeightSubtitle": "Maksymalna waga, jaką ścieżka może osiągnąć dzięki udanym dostarczeniom", - "appSettings_routeWeightSuccessIncrement": "Przyrost wagi po sukcesie", - "appSettings_routeWeightSuccessIncrementSubtitle": "Waga dodawana do ścieżki po udanym dostarczeniu", - "appSettings_routeWeightFailureDecrement": "Spadek wagi po niepowodzeniu", - "appSettings_routeWeightFailureDecrementSubtitle": "Waga odejmowana od ścieżki po nieudanym dostarczeniu", - "appSettings_maxMessageRetries": "Maksymalna liczba ponowień wiadomości", - "appSettings_maxMessageRetriesSubtitle": "Liczba prób ponowienia przed oznaczeniem wiadomości jako nieudanej", - "path_routeWeight": "{weight}/{max}" + "settings_allowByContact": "Zezwalaj według flag kontaktowych", + "settings_allowAll": "Zezwalaj na wszystko", + "settings_telemetryLocationMode": "Tryb położenia telemetrycznego", + "settings_telemetryEnvironmentMode": "Tryb środowiska telemetrycznego", + "settings_advertLocation": "Lokalizacja reklamowa", + "settings_advertLocationSubtitle": "Uwzględnij lokalizację w ogłoszeniu", + "settings_denyAll": "Odmów wszystkim", + "settings_privacySubtitle": "Kontroluj jakie informacje są udostępniane.", + "settings_privacy": "Ustawienia prywatności", + "settings_privacySettingsDescription": "Wybierz jakie informacje urządzenie udostępni innym.", + "contact_info": "Informacje kontaktowe", + "settings_telemetryBaseMode": "Tryb podstawowy telemetrii", + "contact_teleBase": "Baza telemetryczna", + "contact_teleLoc": "Lokalizacja telemetryczna", + "contact_teleLocSubtitle": "Zezwalaj na udostępnianie danych lokalizacji", + "contact_teleEnv": "Środowisko telemetryczne", + "contact_teleEnvSubtitle": "Zezwalaj na udostępnianie danych czujników środowiskowych", + "contact_telemetry": "Telemetryka", + "contact_clearChat": "Wyczyść czat", + "contact_settings": "Ustawienia kontaktowe", + "contact_lastSeen": "Ostatnio widziany", + "contact_teleBaseSubtitle": "Pozwól na udostępnianie poziomu naładowania baterii i podstawowych danych telemetrycznych", + "@settings_multiAck": { + "placeholders": { + "value": { + "type": "String" + } + } + }, + "appSettings_initialRouteWeight": "Początkowa waga trasy", + "appSettings_maxRouteWeight": "Maksymalny dopuszczalny ciężar pojazdu", + "appSettings_initialRouteWeightSubtitle": "Początkowa waga dla nowych, odkrytych ścieżek", + "appSettings_maxRouteWeightSubtitle": "Maksymalna waga, jaką ścieżka może zgromadzić dzięki udanym dostawom.", + "appSettings_routeWeightSuccessIncrement": "Wzrost wagi sukcesu", + "appSettings_routeWeightSuccessIncrementSubtitle": "Waga dodana do ścieżki po pomyślnym dostarczeniu", + "appSettings_routeWeightFailureDecrement": "Zmniejszenie wagi kary", + "appSettings_routeWeightFailureDecrementSubtitle": "Waga usunięta z trasy po nieudanej dostawie", + "appSettings_maxMessageRetries": "Maksymalna liczba prób wysłania wiadomości", + "appSettings_maxMessageRetriesSubtitle": "Liczba prób ponownego wysłania wiadomości przed oznaczaniem jej jako nieudanej", + "path_routeWeight": "{weight}/{max}", + "settings_telemetryModeUpdated": "Tryb telemetryczny zaktualizowany", + "settings_multiAck": "Wiele potwierdzeń: {value}" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index e7e2ec6..adddd13 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1900,6 +1900,35 @@ } } }, + "settings_privacySettingsDescription": "Escolha quais informações o seu dispositivo compartilha com os outros.", + "settings_allowByContact": "Permitir por bandeiras de contato", + "settings_telemetryLocationMode": "Modo de Localização de Telemetria", + "settings_telemetryEnvironmentMode": "Modo de Ambiente de Telemetria", + "settings_advertLocation": "Localização do Anúncio", + "settings_advertLocationSubtitle": "Incluir localização no anúncio", + "settings_privacySubtitle": "Controle o que é compartilhado.", + "settings_denyAll": "Negar todos", + "settings_allowAll": "Permitir todos", + "settings_privacy": "Configurações de Privacidade", + "contact_info": "Informações de Contato", + "settings_telemetryBaseMode": "Modo Base de Telemetria", + "contact_teleBase": "Base de Telemetria", + "contact_teleLoc": "Localização de Telemetria", + "contact_teleLocSubtitle": "Permitir compartilhamento de dados de localização", + "contact_teleEnv": "Ambiente de Telemetria", + "contact_teleEnvSubtitle": "Permitir compartilhamento de dados do sensor de ambiente", + "contact_lastSeen": "Visto pela última vez", + "contact_clearChat": "Limpar Chat", + "contact_telemetry": "Telemetria", + "contact_settings": "Configurações de Contato", + "contact_teleBaseSubtitle": "Permitir compartilhamento do nível da bateria e telemetria básica", + "@settings_multiAck": { + "placeholders": { + "value": { + "type": "String" + } + } + }, "appSettings_initialRouteWeight": "Peso Inicial da Rota", "appSettings_maxRouteWeight": "Peso Máximo da Rota", "appSettings_maxRouteWeightSubtitle": "Peso máximo que um determinado percurso pode acumular com entregas bem-sucedidas.", @@ -1910,5 +1939,7 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Peso removido de um caminho após uma tentativa de entrega malsucedida.", "appSettings_maxMessageRetries": "Número máximo de tentativas de envio de mensagens", "appSettings_maxMessageRetriesSubtitle": "Número de tentativas de reenvio antes de classificar uma mensagem como falha.", - "path_routeWeight": "{weight}/{max}" -} + "path_routeWeight": "{weight}/{max}", + "settings_telemetryModeUpdated": "Modo de telemetria atualizado", + "settings_multiAck": "Multi-ACKs: {value}" +} \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 92a3800..2d3df51 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1140,6 +1140,35 @@ } } }, + "settings_privacy": "Настройки конфиденциальности", + "settings_privacySubtitle": "Контролируйте, какую информацию делиться.", + "settings_telemetryLocationMode": "Режим местоположения телеметрии", + "settings_telemetryEnvironmentMode": "Режим среды телеметрии", + "settings_advertLocation": "Местоположение рекламы", + "settings_advertLocationSubtitle": "Включить местоположение в объявление", + "settings_allowAll": "Разрешить все", + "settings_privacySettingsDescription": "Выберите, какую информацию ваше устройство будет делиться с другими.", + "settings_denyAll": "Отклонить все", + "settings_allowByContact": "Разрешить по флагам контактов", + "contact_info": "Контактная информация", + "settings_telemetryBaseMode": "Базовый режим телеметрии", + "contact_teleBase": "База телеметрии", + "contact_teleLoc": "Местоположение телеметрии", + "contact_teleLocSubtitle": "Разрешить обмен данными о местоположении", + "contact_teleEnv": "Среда телеметрии", + "contact_teleEnvSubtitle": "Разрешить обмен данными датчиков окружающей среды", + "contact_settings": "Настройки контактов", + "contact_telemetry": "Телеметрия", + "contact_clearChat": "Очистить чат", + "contact_lastSeen": "Последний раз видели", + "contact_teleBaseSubtitle": "Разрешить обмен уровнем заряда батареи и базовой телеметрией", + "@settings_multiAck": { + "placeholders": { + "value": { + "type": "String" + } + } + }, "appSettings_maxRouteWeight": "Максимальный допустимый вес маршрута", "appSettings_maxRouteWeightSubtitle": "Максимальный вес, который может быть перевезён по определённому маршруту при успешных доставках.", "appSettings_initialRouteWeightSubtitle": "Начальный вес для новых, только что открытых маршрутов", @@ -1150,5 +1179,7 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Вес, который был удален с пути после неудачной доставки.", "appSettings_maxMessageRetries": "Максимальное количество повторных попыток отправки сообщения", "appSettings_maxMessageRetriesSubtitle": "Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.", - "path_routeWeight": "{weight}/{max}" -} + "path_routeWeight": "{weight}/{max}", + "settings_telemetryModeUpdated": "Режим телеметрии обновлен", + "settings_multiAck": "Мульти-ACK: {value}" +} \ No newline at end of file diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 75a7c7d..57eb285 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1900,6 +1900,35 @@ } } }, + "settings_privacy": "Nastavenia súkromia", + "settings_privacySubtitle": "Ovládni, aké informácie sa zdieľajú.", + "settings_telemetryLocationMode": "Režim umiestnenia telemetrie", + "settings_telemetryBaseMode": "Základný režim telemetrie", + "settings_advertLocation": "Umiestnenie inzerátu", + "settings_telemetryEnvironmentMode": "Režim prostredia telemetrie", + "settings_advertLocationSubtitle": "Zahrnúť polohu do inzerátu", + "settings_allowAll": "Povoliť všetko", + "settings_privacySettingsDescription": "Vyberte, ktoré informácie váš zariadenie zdieľa s ostatnými.", + "settings_denyAll": "Zamietnuť všetko", + "settings_allowByContact": "Povoliť podľa kontaktových vlajok", + "contact_info": "Kontaktné informácie", + "contact_settings": "Nastavenia kontaktov", + "contact_teleBaseSubtitle": "Povoliť zdieľanie úrovne batérie a základnej telemetrie", + "contact_teleLoc": "Lokácia telemetrie", + "contact_teleLocSubtitle": "Povoliť zdieľanie údajov o lokalite", + "contact_teleEnv": "Prostredie telemetrie", + "contact_telemetry": "Telemetria", + "contact_clearChat": "Vymazať chat", + "contact_lastSeen": "Naposledy videný", + "contact_teleBase": "Báza telemetrie", + "contact_teleEnvSubtitle": "Povoliť zdieľanie údajov senzorov prostredia", + "@settings_multiAck": { + "placeholders": { + "value": { + "type": "String" + } + } + }, "appSettings_maxRouteWeightSubtitle": "Maximálna hmotnosť, ktorú môže trás prenášať vďaka úspešným zásielkam.", "appSettings_initialRouteWeightSubtitle": "Počiatočná váha pre nové, objavené cesty", "appSettings_initialRouteWeight": "Počiatočná váha trasy", @@ -1910,5 +1939,7 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Hmotnosť odstránená z cesty po neúspešnej doručenie", "appSettings_maxMessageRetries": "Maximalný počet pokusov o doručenie správ", "appSettings_maxMessageRetriesSubtitle": "Počet pokusov o odošleť pred označením správy ako neúspešnej", - "path_routeWeight": "{weight}/{max}" -} + "path_routeWeight": "{weight}/{max}", + "settings_telemetryModeUpdated": "Režim telemetrie bol aktualizovaný", + "settings_multiAck": "Viaceré ACK: {value}" +} \ No newline at end of file diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 5ab4736..355f8d8 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1900,6 +1900,35 @@ } } }, + "settings_privacy": "Nastavitve zasebnosti", + "settings_privacySettingsDescription": "Izberite, katere informacije vaš naprava deli z drugimi.", + "settings_telemetryBaseMode": "Osnovni način telemetrije", + "settings_telemetryLocationMode": "Način delovanja telemetrije", + "settings_telemetryEnvironmentMode": "Način delovanja okolja telemetrije", + "settings_advertLocation": "Lokacija oglasa", + "settings_allowByContact": "Dovoli po kontaktnih zastavah", + "settings_denyAll": "Zavrniti vse", + "settings_allowAll": "Dovoli vse", + "settings_privacySubtitle": "Kontrolirajte, katere informacije so deljene.", + "contact_info": "Kontaktni podatki", + "contact_teleBase": "Baza telemetrije", + "contact_teleBaseSubtitle": "Dovoli deljenje stanja baterije in osnovne telemetrije", + "contact_teleLoc": "Lokacija telemetrije", + "contact_lastSeen": "Zadnjič videno", + "contact_settings": "Nastavitve stika", + "settings_advertLocationSubtitle": "Vključi lokacijo v oglas.", + "contact_telemetry": "Telemetrija", + "contact_clearChat": "Počisti klepet", + "contact_teleEnv": "Okolje telemetrije", + "contact_teleEnvSubtitle": "Dovoli deljenje podatkov okoljskih senzorjev", + "contact_teleLocSubtitle": "Dovoli deljenje podatkov o lokaciji", + "@settings_multiAck": { + "placeholders": { + "value": { + "type": "String" + } + } + }, "appSettings_maxRouteWeightSubtitle": "Največja teža, ki jo lahko pot doseže s uspešnimi dostavnami.", "appSettings_initialRouteWeight": "Izvirna teža poti", "appSettings_initialRouteWeightSubtitle": "Izguba teže za nove, odkriti poti", @@ -1910,5 +1939,7 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Težo, ki ni bila uspešno dostavljena, odstranili s poti.", "appSettings_maxMessageRetries": "Najve število poskusov pošiljanja sporočil", "appSettings_maxMessageRetriesSubtitle": "Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno", - "path_routeWeight": "{weight}/{max}" -} + "path_routeWeight": "{weight}/{max}", + "settings_multiAck": "Večkratni potrditvi: {value}", + "settings_telemetryModeUpdated": "Način telemetrije posodobljen" +} \ No newline at end of file diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 644b43b..84f4e5e 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1900,6 +1900,35 @@ } } }, + "settings_privacy": "Inställningar för sekretess", + "settings_allowAll": "Tillåt alla", + "settings_privacySubtitle": "Kontrollera vilken information som delas.", + "settings_telemetryEnvironmentMode": "Telemetri miljöläge", + "settings_telemetryBaseMode": "Telemetribasläge", + "settings_telemetryLocationMode": "Telemetritillstånd för plats", + "settings_advertLocation": "Annonsplacering", + "contact_info": "Kontaktinformation", + "contact_settings": "Kontaktinställningar", + "contact_telemetry": "Telemetri", + "settings_denyAll": "Neka alla", + "settings_allowByContact": "Tillåt via kontaktflaggor", + "settings_privacySettingsDescription": "Välj vilken information din enhet delar med andra.", + "contact_lastSeen": "Senast sedd", + "contact_clearChat": "Rensa Chatt", + "contact_teleEnv": "Telemetri Miljö", + "settings_advertLocationSubtitle": "Inkludera plats i annonsen", + "contact_teleEnvSubtitle": "Tillåt delning av miljösensordata", + "contact_teleBase": "Telemetribas", + "contact_teleBaseSubtitle": "Tillåt delning av batterinivå och grundläggande telemetri", + "contact_teleLoc": "Telemetridata plats", + "contact_teleLocSubtitle": "Tillåt delning av platsdata", + "@settings_multiAck": { + "placeholders": { + "value": { + "type": "String" + } + } + }, "appSettings_initialRouteWeightSubtitle": "Initial vikt för nyligen upptäckta vägar", "appSettings_maxRouteWeight": "Maximalt tillåtet vikt för rutten", "appSettings_maxRouteWeightSubtitle": "Maximal vikt som en leveransväg kan ackumulera från framgångsrika leveranser.", @@ -1910,5 +1939,7 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Vikt som tagits bort från en väg efter ett misslyckat leveransförsök", "appSettings_maxMessageRetries": "Maximalt antal försök", "appSettings_maxMessageRetriesSubtitle": "Antal försök att skicka om ett meddelande innan det markeras som misslyckat.", - "path_routeWeight": "{weight}/{max}" -} + "path_routeWeight": "{weight}/{max}", + "settings_telemetryModeUpdated": "Telemetri-läge uppdaterat", + "settings_multiAck": "Multi-ACKs: {value}" +} \ No newline at end of file diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 249fd3b..be1eaa8 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1900,6 +1900,35 @@ } } }, + "settings_privacySubtitle": "Керуйте інформацією, яку буде спільно використовуватися", + "settings_privacy": "Налаштування приватності", + "settings_telemetryBaseMode": "Режим базової телеметрії", + "settings_telemetryLocationMode": "Режим місця телеметрії", + "settings_advertLocation": "Розміщення реклами", + "settings_advertLocationSubtitle": "Включити місце розташування в оголошення", + "settings_privacySettingsDescription": "Виберіть, яку інформацію ваш пристрій буде передавати іншим.", + "settings_allowAll": "Дозволити все", + "settings_denyAll": "Відхилити все", + "settings_allowByContact": "Дозволити за контактними прапорцями", + "settings_telemetryEnvironmentMode": "Режим середовища телеметрії", + "contact_info": "Контактна інформація", + "contact_teleBaseSubtitle": "Дозволити спільний доступ до рівня заряду батареї та базової телеметрії", + "contact_teleLoc": "Розташування телеметрії", + "contact_teleBase": "Базовий телебачення", + "contact_teleLocSubtitle": "Дозволити спільне використання даних про місцеположення", + "contact_settings": "Налаштування контактів", + "contact_telemetry": "Телеметрія", + "contact_clearChat": "Очистити чат", + "contact_lastSeen": "Останній раз бачили", + "contact_teleEnv": "Середовище телеметрії", + "contact_teleEnvSubtitle": "Дозволити спільний доступ до даних датчиків середовища", + "@settings_multiAck": { + "placeholders": { + "value": { + "type": "String" + } + } + }, "appSettings_initialRouteWeight": "Початкова вартість маршруту", "appSettings_initialRouteWeightSubtitle": "Початкова вага для нових відкритих шляхів", "appSettings_maxRouteWeight": "Максимальна вага маршруту", @@ -1910,5 +1939,7 @@ "appSettings_routeWeightFailureDecrementSubtitle": "Вага, яка була знята з маршруту після невдалої доставки", "appSettings_maxMessageRetries": "Максимальна кількість повторних спроб надсилання повідомлення", "appSettings_maxMessageRetriesSubtitle": "Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале", - "path_routeWeight": "{weight}/{max}" -} + "path_routeWeight": "{weight}/{max}", + "settings_telemetryModeUpdated": "Режим телеметрії оновлено", + "settings_multiAck": "Багатократне підтвердження: {value}" +} \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 1d4ed30..9493b27 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1905,6 +1905,35 @@ } } }, + "settings_privacySubtitle": "控制要共享的信息。", + "settings_privacySettingsDescription": "选择您的设备与他人共享的信息。", + "settings_telemetryBaseMode": "遥测基础模式", + "settings_telemetryLocationMode": "遥测位置模式", + "settings_advertLocation": "广告位置", + "settings_advertLocationSubtitle": "在广告中包含位置", + "settings_allowByContact": "按联系人标志允许", + "settings_denyAll": "拒绝所有", + "settings_privacy": "隐私设置", + "settings_allowAll": "允许全部", + "contact_info": "联系信息", + "contact_teleBase": "遥测基站", + "contact_teleBaseSubtitle": "允许共享电池电量和基本遥测数据", + "settings_telemetryEnvironmentMode": "遥测环境模式", + "contact_teleLoc": "遥测位置", + "contact_teleEnv": "遥测环境", + "contact_teleEnvSubtitle": "允许共享环境传感器数据", + "contact_clearChat": "清除聊天记录", + "contact_lastSeen": "最近出现", + "contact_settings": "联系人设置", + "contact_teleLocSubtitle": "允许共享位置数据", + "contact_telemetry": "遥测数据", + "@settings_multiAck": { + "placeholders": { + "value": { + "type": "String" + } + } + }, "appSettings_maxRouteWeight": "最大路径重量", "appSettings_initialRouteWeightSubtitle": "新发现路径的初始重量", "appSettings_initialRouteWeight": "初始路线权重", @@ -1915,5 +1944,7 @@ "appSettings_routeWeightFailureDecrementSubtitle": "从一条路径上移除的货物,由于无法成功交付而移除。", "appSettings_maxMessageRetries": "最大消息重试次数", "appSettings_maxMessageRetriesSubtitle": "在将消息标记为失败之前,允许尝试的次数", - "path_routeWeight": "{weight}/{max}" -} + "path_routeWeight": "{weight}/{max}", + "settings_multiAck": "多重ACK:{value}", + "settings_telemetryModeUpdated": "遥测模式已更新" +} \ No newline at end of file diff --git a/lib/models/channel.dart b/lib/models/channel.dart index 1a2ecdc..4fdd627 100644 --- a/lib/models/channel.dart +++ b/lib/models/channel.dart @@ -24,20 +24,23 @@ class Channel { bool get isPublicChannel => pskHex == publicChannelPsk; - static Channel? fromFrame(Uint8List data) { + static Channel? fromFrame(Uint8List frame) { // CHANNEL_INFO format: // [0] = RESP_CODE_CHANNEL_INFO (18) // [1] = channel_idx // [2-33] = name (32 bytes, null-terminated) // [34-49] = psk (16 bytes) - if (data.length < 50) return null; - if (data[0] != respCodeChannelInfo) return null; - - final index = data[1]; - final name = readCString(data, 2, 32); - final psk = Uint8List.fromList(data.sublist(34, 50)); - - return Channel(index: index, name: name, psk: psk); + if (frame.length < 50) return null; + final reader = BufferReader(frame); + try { + if (reader.readByte() != respCodeChannelInfo) return null; + final index = reader.readByte(); + final name = reader.readCStringGreedy(32); + final psk = reader.readBytes(16); + return Channel(index: index, name: name, psk: psk); + } catch (e) { + return null; + } } static Channel empty(int index) { diff --git a/lib/models/channel_message.dart b/lib/models/channel_message.dart index b0af3eb..7c09089 100644 --- a/lib/models/channel_message.dart +++ b/lib/models/channel_message.dart @@ -2,6 +2,7 @@ import 'dart:typed_data'; import '../connector/meshcore_protocol.dart'; import '../helpers/reaction_helper.dart'; import '../helpers/smaz.dart'; +import '../utils/app_logger.dart'; enum ChannelMessageStatus { pending, sent, failed } @@ -109,89 +110,82 @@ class ChannelMessage { ); } - static ChannelMessage? fromFrame(Uint8List data) { + static ChannelMessage? fromFrame(Uint8List frame) { // CHANNEL_MSG_RECV format varies by version: // V3: [0]=code [1]=SNR [2]=rsv1 [3]=rsv2 [4]=channel_idx [5]=path_len [path... optional] [txt_type] [timestamp x4] [text...] // Non-V3: [0]=code [1]=channel_idx [2]=path_len [3]=txt_type [4-7]=timestamp [8+]=text - if (data.length < 8) return null; + if (frame.length < 8) return null; + try { + final reader = BufferReader(frame); + final code = reader.readByte(); + if (code != respCodeChannelMsgRecv && code != respCodeChannelMsgRecvV3) { + return null; + } - final code = data[0]; - if (code != respCodeChannelMsgRecv && code != respCodeChannelMsgRecvV3) { + int pathLen; + int txtType; + Uint8List pathBytes = Uint8List(0); + int channelIdx; + if (code == respCodeChannelMsgRecvV3) { + reader.skipBytes(1); // Skip SNR + final flags = reader.readByte(); + final hasPath = (flags & 0x01) != 0; + reader.skipBytes(1); // Skip reserved byte + channelIdx = reader.readByte(); + pathLen = reader.readInt8(); + txtType = reader.readByte(); + if (hasPath && pathLen > 0) { + reader.rewind(); // Rewind to read path length again for pathBytes + pathBytes = reader.readBytes(pathLen); + } + } else { + channelIdx = reader.readByte(); + pathLen = reader.readInt8(); + txtType = reader.readByte(); + } + final timestampRaw = reader.readUInt32LE(); + + if (txtType != txtTypePlain) { + return null; + } + + final text = reader.readCString(); + + // Extract sender name and actual message from "name: msg" format + String senderName = 'Unknown'; + String actualText = text; + + final colonIndex = text.indexOf(':'); + if (colonIndex > 0 && colonIndex < text.length - 1 && colonIndex < 50) { + final potentialSender = text.substring(0, colonIndex); + if (!RegExp(r'[:\[\]]').hasMatch(potentialSender)) { + senderName = potentialSender; + final offset = + (colonIndex + 1 < text.length && text[colonIndex + 1] == ' ') + ? colonIndex + 2 + : colonIndex + 1; + actualText = text.substring(offset); + } + } + + final decodedText = Smaz.tryDecodePrefixed(actualText) ?? actualText; + + return ChannelMessage( + senderKey: null, + senderName: senderName, + text: decodedText, + timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000), + isOutgoing: false, + status: ChannelMessageStatus.sent, + pathLength: pathLen, + pathBytes: pathBytes, + channelIndex: channelIdx, + ); + } catch (e) { + appLogger.error('Error parsing channel message frame: $e'); + // If parsing fails, return null to avoid crashes return null; } - - int timestampOffset, textOffset, pathLenOffset, txtTypeOffset; - Uint8List pathBytes = Uint8List(0); - int channelIdx; - - if (code == respCodeChannelMsgRecvV3) { - channelIdx = data[4]; - pathLenOffset = 5; - final pathLen = data[pathLenOffset].toSigned(8); - var cursor = 6; - final hasPathBytesFlag = (data[2] & 0x01) != 0; - final canFitPath = pathLen > 0 && data.length >= cursor + pathLen + 5; - final hasValidTxtType = - cursor < data.length && - (data[cursor] == txtTypePlain || data[cursor] == txtTypeCliData); - if ((hasPathBytesFlag || (canFitPath && !hasValidTxtType)) && - canFitPath) { - pathBytes = Uint8List.fromList(data.sublist(cursor, cursor + pathLen)); - cursor += pathLen; - } - txtTypeOffset = cursor; - cursor += 1; // txt_type - timestampOffset = cursor; - textOffset = cursor + 4; - } else { - channelIdx = data[1]; - pathLenOffset = 2; - txtTypeOffset = 3; - timestampOffset = 4; - textOffset = 8; - } - - if (data.length < textOffset + 1) return null; - - final txtType = data[txtTypeOffset]; - if (txtType != txtTypePlain) { - return null; - } - - final pathLen = data[pathLenOffset].toSigned(8); - final timestampRaw = readUint32LE(data, timestampOffset); - final text = readCString(data, textOffset, data.length - textOffset); - - // Extract sender name and actual message from "name: msg" format - String senderName = 'Unknown'; - String actualText = text; - - final colonIndex = text.indexOf(':'); - if (colonIndex > 0 && colonIndex < text.length - 1 && colonIndex < 50) { - final potentialSender = text.substring(0, colonIndex); - if (!RegExp(r'[:\[\]]').hasMatch(potentialSender)) { - senderName = potentialSender; - final offset = - (colonIndex + 1 < text.length && text[colonIndex + 1] == ' ') - ? colonIndex + 2 - : colonIndex + 1; - actualText = text.substring(offset); - } - } - - final decodedText = Smaz.tryDecodePrefixed(actualText) ?? actualText; - - return ChannelMessage( - senderKey: null, - senderName: senderName, - text: decodedText, - timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000), - isOutgoing: false, - status: ChannelMessageStatus.sent, - pathLength: pathLen, - pathBytes: pathBytes, - channelIndex: channelIdx, - ); } static ChannelMessage outgoing( diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 71467b1..5c80893 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -18,6 +18,7 @@ class Contact { final DateTime lastSeen; final DateTime lastMessageAt; final bool isActive; + final bool wasPulled; final Uint8List? rawPacket; Contact({ @@ -34,6 +35,7 @@ class Contact { required this.lastSeen, DateTime? lastMessageAt, this.isActive = true, + this.wasPulled = false, this.rawPacket, }) : lastMessageAt = lastMessageAt ?? lastSeen; @@ -214,4 +216,7 @@ class Contact { @override int get hashCode => publicKeyHex.hashCode; + bool get teleBaseEnabled => (flags & contactFlagTeleBase) != 0; + bool get teleLocEnabled => (flags & contactFlagTeleLoc) != 0; + bool get teleEnvEnabled => (flags & contactFlagTeleEnv) != 0; } diff --git a/lib/models/message.dart b/lib/models/message.dart index 6f6ed88..6b930c0 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -16,7 +16,7 @@ class Message { final String? messageId; final int retryCount; final int? estimatedTimeoutMs; - final Uint8List? expectedAckHash; + final int? expectedAckHash; final DateTime? sentAt; final DateTime? deliveredAt; final int? tripTimeMs; @@ -56,7 +56,7 @@ class Message { MessageStatus? status, int? retryCount, int? estimatedTimeoutMs, - Uint8List? expectedAckHash, + int? expectedAckHash, DateTime? sentAt, DateTime? deliveredAt, int? tripTimeMs, @@ -90,33 +90,35 @@ class Message { ); } - static Message? fromFrame(Uint8List data, Uint8List selfPubKey) { - if (data.length < msgTextOffset + 1) return null; + static Message? fromFrame(Uint8List frame, Uint8List selfPubKey) { + if (frame.length < msgTextOffset + 1) return null; + final reader = BufferReader(frame); + try { + final code = reader.readByte(); + if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) { + return null; + } - final code = data[0]; - if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) { + final senderKey = reader.readBytes(pubKeySize); + final timestampRaw = reader.readInt32LE(); + final flags = reader.readByte(); + if ((flags >> 2) != txtTypePlain) { + return null; + } + final text = reader.readCString(); + + return Message( + senderKey: senderKey, + text: text, + timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000), + isOutgoing: false, + isCli: false, + status: MessageStatus.delivered, + pathBytes: Uint8List(0), + ); + } catch (e) { return null; } - - final senderKey = Uint8List.fromList( - data.sublist(msgPubKeyOffset, msgPubKeyOffset + pubKeySize), - ); - final timestampRaw = readUint32LE(data, msgTimestampOffset); - final flags = data[msgFlagsOffset]; - if ((flags >> 2) != txtTypePlain) { - return null; - } - final text = readCString(data, msgTextOffset, data.length - msgTextOffset); - - return Message( - senderKey: senderKey, - text: text, - timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000), - isOutgoing: false, - isCli: false, - status: MessageStatus.delivered, - pathBytes: Uint8List(0), - ); } static Message outgoing( diff --git a/lib/screens/ble_debug_log_screen.dart b/lib/screens/ble_debug_log_screen.dart index a90f9f0..1009bc4 100644 --- a/lib/screens/ble_debug_log_screen.dart +++ b/lib/screens/ble_debug_log_screen.dart @@ -283,66 +283,66 @@ class _BleDebugLogScreenState extends State { if (payload.length < 101) { return 'ADVERT (short)'; } - var offset = 0; - final pubKey = _bytesToHex( - payload.sublist(offset, offset + 32), - spaced: false, - ); - offset += 32; - final timestamp = readUint32LE(payload, offset); - offset += 4; - offset += 64; // signature - final flags = payload[offset++]; - final role = _deviceRoleLabel(flags & 0x0F); - final hasLocation = (flags & 0x10) != 0; - final hasFeature1 = (flags & 0x20) != 0; - final hasFeature2 = (flags & 0x40) != 0; - final hasName = (flags & 0x80) != 0; - String? name; - double? lat; - double? lon; - if (hasLocation && payload.length >= offset + 8) { - lat = readInt32LE(payload, offset) / 1000000.0; - lon = readInt32LE(payload, offset + 4) / 1000000.0; - offset += 8; + final reader = BufferReader(payload); + try { + final pubKey = _bytesToHex(reader.readBytes(pubKeySize), spaced: false); + + final timestamp = reader.readUInt32LE(); + reader.skipBytes(signatureSize); + final flags = reader.readByte(); + final role = _deviceRoleLabel(flags & 0x0F); + final hasLocation = (flags & 0x10) != 0; + final hasFeature1 = (flags & 0x20) != 0; + final hasFeature2 = (flags & 0x40) != 0; + final hasName = (flags & 0x80) != 0; + String? name; + double? lat; + double? lon; + if (hasLocation) { + lat = reader.readInt32LE() / 1000000.0; + lon = reader.readInt32LE() / 1000000.0; + } + if (hasFeature1) reader.skipBytes(2); + if (hasFeature2) reader.skipBytes(2); + if (hasName) { + name = reader.readCStringGreedy(maxNameSize); + } + final namePart = (name != null && name.isNotEmpty) ? ' name="$name"' : ''; + final locPart = (lat != null && lon != null) + ? ' loc=${lat.toStringAsFixed(6)},${lon.toStringAsFixed(6)}' + : ''; + return 'ADVERT role=$role ts=$timestamp$namePart$locPart key=${pubKey.substring(0, 12)}…'; + } catch (e) { + return 'ADVERT (invalid)'; } - if (hasFeature1) offset += 2; - if (hasFeature2) offset += 2; - if (hasName && payload.length > offset) { - final rawName = String.fromCharCodes(payload.sublist(offset)); - final nul = rawName.indexOf('\u0000'); - name = nul >= 0 ? rawName.substring(0, nul) : rawName; - name = name.trim(); - } - final namePart = (name != null && name.isNotEmpty) ? ' name="$name"' : ''; - final locPart = (lat != null && lon != null) - ? ' loc=${lat.toStringAsFixed(6)},${lon.toStringAsFixed(6)}' - : ''; - return 'ADVERT role=$role ts=$timestamp$namePart$locPart key=${pubKey.substring(0, 12)}…'; } String _decodeControlSummary(Uint8List payload) { - if (payload.isEmpty) return 'CONTROL (empty)'; - final flags = payload[0]; - final subType = flags & 0xF0; - if (subType == 0x80) { - if (payload.length < 6) return 'CONTROL DISCOVER_REQ (short)'; - final typeFilter = payload[1]; - final tag = readUint32LE(payload, 2); - final since = payload.length >= 10 ? readUint32LE(payload, 6) : 0; - return 'CONTROL DISCOVER_REQ filter=0x${typeFilter.toRadixString(16).padLeft(2, '0')} tag=$tag since=$since'; + final reader = BufferReader(payload); + try { + final flags = reader.readByte(); + final subType = flags & 0xF0; + if (subType == 0x80) { + if (payload.length < 6) return 'CONTROL DISCOVER_REQ (short)'; + final typeFilter = reader.readByte(); + final tag = reader.readInt32LE(); + final since = payload.length >= 10 ? reader.readInt32LE() : 0; + return 'CONTROL DISCOVER_REQ filter=0x${typeFilter.toRadixString(16).padLeft(2, '0')} tag=$tag since=$since'; + } + if (subType == 0x90) { + if (payload.length < 14) return 'CONTROL DISCOVER_RESP (short)'; + final nodeType = flags & 0x0F; + final snrRaw = payload[1]; + final snrSigned = snrRaw > 127 ? snrRaw - 256 : snrRaw; + final snr = snrSigned / 4.0; + final tag = reader.readInt32LE(); + final keyLen = payload.length - 6; + return 'CONTROL DISCOVER_RESP node=${_deviceRoleLabel(nodeType)} snr=${snr.toStringAsFixed(2)} tag=$tag key=$keyLen'; + } + return 'CONTROL subtype=0x${subType.toRadixString(16).padLeft(2, '0')}'; + } catch (e) { + return 'CONTROL (invalid)'; } - if (subType == 0x90) { - if (payload.length < 14) return 'CONTROL DISCOVER_RESP (short)'; - final nodeType = flags & 0x0F; - final snrRaw = payload[1]; - final snrSigned = snrRaw > 127 ? snrRaw - 256 : snrRaw; - final snr = snrSigned / 4.0; - final tag = readUint32LE(payload, 2); - final keyLen = payload.length - 6; - return 'CONTROL DISCOVER_RESP node=${_deviceRoleLabel(nodeType)} snr=${snr.toStringAsFixed(2)} tag=$tag key=$keyLen'; - } - return 'CONTROL subtype=0x${subType.toRadixString(16).padLeft(2, '0')}'; } String _payloadTypeLabel(int payloadType) { diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 20110e1..913b288 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -166,6 +166,33 @@ class _ChannelChatScreenState extends State { ], ), centerTitle: false, + actions: [ + PopupMenuButton( + icon: const Icon(Icons.more_vert), + onSelected: (value) { + if (value == 'clearChat') { + context.read().clearMessagesForChannel( + widget.channel.index, + ); + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'clearChat', + child: Row( + children: [ + const Icon(Icons.delete, size: 20, color: Colors.red), + const SizedBox(width: 12), + Text( + context.l10n.contact_clearChat, + style: const TextStyle(color: Colors.red), + ), + ], + ), + ), + ], + ), + ], ), body: SafeArea( top: false, diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index ace82b5..574ffbe 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -38,6 +38,7 @@ import '../widgets/gif_picker.dart'; import '../widgets/path_selection_dialog.dart'; import '../utils/app_logger.dart'; import '../l10n/l10n.dart'; +import 'telemetry_screen.dart'; class ChatScreen extends StatefulWidget { final Contact contact; @@ -246,9 +247,77 @@ class _ChatScreenState extends State { tooltip: context.l10n.chat_pathManagement, onPressed: () => _showPathHistory(context), ), - IconButton( - icon: const Icon(Icons.info_outline), - onPressed: () => _showContactInfo(context), + Consumer( + builder: (context, connector, _) { + return PopupMenuButton( + icon: const Icon(Icons.more_vert), + onSelected: (value) { + if (value == 'info') { + _showContactInfo(context); + } + if (value == 'settings') { + _showContactSettings(context); + } + if (value == 'telemetry') { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + TelemetryScreen(contact: widget.contact), + ), + ); + } + if (value == 'clearChat') { + connector.clearMessagesForContact(widget.contact); + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'info', + child: Row( + children: [ + const Icon(Icons.info_outline, size: 20), + const SizedBox(width: 12), + Text(context.l10n.contact_info), + ], + ), + ), + PopupMenuItem( + value: 'telemetry', + child: Row( + children: [ + const Icon(Icons.bar_chart, size: 20), + const SizedBox(width: 12), + Text(context.l10n.contact_telemetry), + ], + ), + ), + PopupMenuItem( + value: 'settings', + child: Row( + children: [ + const Icon(Icons.settings, size: 20), + const SizedBox(width: 12), + Text(context.l10n.contact_settings), + ], + ), + ), + PopupMenuItem( + value: 'clearChat', + child: Row( + children: [ + const Icon(Icons.delete, size: 20, color: Colors.red), + const SizedBox(width: 12), + Text( + context.l10n.contact_clearChat, + style: const TextStyle(color: Colors.red), + ), + ], + ), + ), + ], + ); + }, ), ], ), @@ -895,11 +964,22 @@ class _ChatScreenState extends State { ); } + int _resolveContactIndex = -1; + Contact _resolveContact(MeshCoreConnector connector) { - return connector.contacts.firstWhere( + if (_resolveContactIndex >= 0 && + _resolveContactIndex < connector.contacts.length && + connector.contacts[_resolveContactIndex].publicKeyHex == + widget.contact.publicKeyHex) { + return connector.contacts[_resolveContactIndex]; + } + _resolveContactIndex = connector.contacts.indexWhere( (c) => c.publicKeyHex == widget.contact.publicKeyHex, - orElse: () => widget.contact, ); + if (_resolveContactIndex == -1) { + return widget.contact; + } + return connector.contacts[_resolveContactIndex]; } Contact _resolveContactFrom4Bytes( @@ -952,59 +1032,127 @@ class _ChatScreenState extends State { void _showContactInfo(BuildContext context) { final connector = Provider.of(context, listen: false); - connector.ensureContactSmazSettingLoaded(widget.contact.publicKeyHex); - + final contact = _resolveContact(connector); showDialog( context: context, - builder: (context) => Consumer( - builder: (context, connector, _) { - final contact = _resolveContact(connector); - final smazEnabled = connector.isContactSmazEnabled( - contact.publicKeyHex, - ); - - return AlertDialog( - title: Text(contact.name), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildInfoRow(context.l10n.chat_type, contact.typeLabel), - _buildInfoRow(context.l10n.chat_path, contact.pathLabel), - if (contact.hasLocation) - _buildInfoRow( - context.l10n.chat_location, - '${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}', - ), - _buildInfoRow( - context.l10n.chat_publicKey, - '${contact.publicKeyHex.substring(0, 16)}...', - ), - const Divider(), - SwitchListTile( - contentPadding: EdgeInsets.zero, - title: Text(context.l10n.channels_smazCompression), - subtitle: Text(context.l10n.chat_compressOutgoingMessages), - value: smazEnabled, - onChanged: (value) { - connector.setContactSmazEnabled( - contact.publicKeyHex, - value, - ); - }, - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(context.l10n.common_close), + builder: (context) => AlertDialog( + title: SelectableText(contact.name), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildInfoRow(context.l10n.chat_type, contact.typeLabel), + _buildInfoRow(context.l10n.chat_path, contact.pathLabel), + _buildInfoRow( + context.l10n.contact_lastSeen, + _formatContactLastMessage(contact.lastMessageAt), ), + if (contact.hasLocation) + _buildInfoRow( + context.l10n.chat_location, + '${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}', + ), + _buildInfoRow(context.l10n.chat_publicKey, contact.publicKeyHex), ], - ); - }, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(context.l10n.common_close), + ), + ], + ), + ); + } + + void _showContactSettings(BuildContext context) { + final connector = Provider.of(context, listen: false); + connector.ensureContactSmazSettingLoaded(widget.contact.publicKeyHex); + final contact = widget.contact; + bool smazEnabled = connector.isContactSmazEnabled(contact.publicKeyHex); + bool teleBaseEnabled = contact.teleBaseEnabled; + bool teleLocEnabled = contact.teleLocEnabled; + bool teleEnvEnabled = contact.teleEnvEnabled; + showDialog( + context: context, + builder: (context) => StatefulBuilder( + builder: (context, setDialogState) => AlertDialog( + title: Text(context.l10n.contact_settings), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (contact.hasLocation) ...[ + _buildInfoRow( + context.l10n.chat_location, + '${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}', + ), + const Divider(height: 8), + ], + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text(context.l10n.channels_smazCompression), + subtitle: Text(context.l10n.chat_compressOutgoingMessages), + value: smazEnabled, + onChanged: (value) { + connector.setContactSmazEnabled( + contact.publicKeyHex, + value, + ); + setDialogState(() => smazEnabled = value); + }, + ), + const Divider(height: 8), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text(context.l10n.contact_teleBase), + subtitle: Text(context.l10n.contact_teleBaseSubtitle), + value: teleBaseEnabled, + onChanged: (value) { + setDialogState(() => teleBaseEnabled = value); + }, + ), + const Divider(height: 8), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text(context.l10n.contact_teleLoc), + subtitle: Text(context.l10n.contact_teleLocSubtitle), + value: teleLocEnabled, + onChanged: (value) { + setDialogState(() => teleLocEnabled = value); + }, + ), + const Divider(height: 8), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text(context.l10n.contact_teleEnv), + subtitle: Text(context.l10n.contact_teleEnvSubtitle), + value: teleEnvEnabled, + onChanged: (value) { + setDialogState(() => teleEnvEnabled = value); + }, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + connector.setContactFlags( + contact, + teleBase: teleBaseEnabled, + teleLoc: teleLocEnabled, + teleEnv: teleEnvEnabled, + ); + Navigator.pop(context); + }, + child: Text(context.l10n.common_close), + ), + ], + ), ), ); } @@ -1019,12 +1167,32 @@ class _ChatScreenState extends State { width: 80, child: Text(label, style: TextStyle(color: Colors.grey[600])), ), - Expanded(child: Text(value)), + Expanded(child: SelectableText(value)), ], ), ); } + String _formatContactLastMessage(DateTime timestamp) { + final diff = DateTime.now().difference(timestamp); + if (diff.isNegative || diff.inMinutes < 5) { + return context.l10n.contacts_lastSeenNow; + } + if (diff.inMinutes < 60) { + return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes); + } + if (diff.inHours < 24) { + final hours = diff.inHours; + return hours == 1 + ? context.l10n.contacts_lastSeenHourAgo + : context.l10n.contacts_lastSeenHoursAgo(hours); + } + final days = diff.inDays; + return days == 1 + ? context.l10n.contacts_lastSeenDayAgo + : context.l10n.contacts_lastSeenDaysAgo(days); + } + void _openChat(BuildContext context, Contact contact) { // Check if this is a repeater context.read().markContactRead(contact.publicKeyHex); diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 011e6d0..17eaa24 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -1354,7 +1354,10 @@ class _ContactsScreenState extends State ), onTap: () async { Navigator.pop(sheetContext); - await connector.setContactFavorite(contact, !isFavorite); + await connector.setContactFlags( + contact, + isFavorite: !isFavorite, + ); }, ), ListTile( diff --git a/lib/screens/neighbors_screen.dart b/lib/screens/neighbors_screen.dart index 5afeda4..f4c1673 100644 --- a/lib/screens/neighbors_screen.dart +++ b/lib/screens/neighbors_screen.dart @@ -44,6 +44,24 @@ class _NeighborsScreenState extends State { PathSelection? _pendingStatusSelection; List>? _parsedNeighbors; + int _resolveRepeaterIndex = -1; + + Contact _resolveRepeater(MeshCoreConnector connector) { + if (_resolveRepeaterIndex >= 0 && + _resolveRepeaterIndex < connector.contacts.length && + connector.contacts[_resolveRepeaterIndex].publicKeyHex == + widget.repeater.publicKeyHex) { + return connector.contacts[_resolveRepeaterIndex]; + } + _resolveRepeaterIndex = connector.contacts.indexWhere( + (c) => c.publicKeyHex == widget.repeater.publicKeyHex, + ); + if (_resolveRepeaterIndex == -1) { + return widget.repeater; + } + return connector.contacts[_resolveRepeaterIndex]; + } + @override void initState() { super.initState(); @@ -163,13 +181,6 @@ class _NeighborsScreenState extends State { } } - Contact _resolveRepeater(MeshCoreConnector connector) { - return connector.contacts.firstWhere( - (c) => c.publicKeyHex == widget.repeater.publicKeyHex, - orElse: () => widget.repeater, - ); - } - Future _loadNeighbors() async { if (_commandService == null) return; diff --git a/lib/screens/repeater_cli_screen.dart b/lib/screens/repeater_cli_screen.dart index 1c7ff43..52d92aa 100644 --- a/lib/screens/repeater_cli_screen.dart +++ b/lib/screens/repeater_cli_screen.dart @@ -77,11 +77,22 @@ class _RepeaterCliScreenState extends State { }); } + int _resolveRepeaterIndex = -1; + Contact _resolveRepeater(MeshCoreConnector connector) { - return connector.contacts.firstWhere( + if (_resolveRepeaterIndex >= 0 && + _resolveRepeaterIndex < connector.contacts.length && + connector.contacts[_resolveRepeaterIndex].publicKeyHex == + widget.repeater.publicKeyHex) { + return connector.contacts[_resolveRepeaterIndex]; + } + _resolveRepeaterIndex = connector.contacts.indexWhere( (c) => c.publicKeyHex == widget.repeater.publicKeyHex, - orElse: () => widget.repeater, ); + if (_resolveRepeaterIndex == -1) { + return widget.repeater; + } + return connector.contacts[_resolveRepeaterIndex]; } void _handleTextMessageResponse(Uint8List frame) { diff --git a/lib/screens/repeater_hub_screen.dart b/lib/screens/repeater_hub_screen.dart index fd2da8e..8a14253 100644 --- a/lib/screens/repeater_hub_screen.dart +++ b/lib/screens/repeater_hub_screen.dart @@ -205,8 +205,7 @@ class RepeaterHubScreen extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => - TelemetryScreen(repeater: repeater, password: password), + builder: (context) => TelemetryScreen(contact: repeater), ), ); }, diff --git a/lib/screens/repeater_settings_screen.dart b/lib/screens/repeater_settings_screen.dart index bae0f50..6375e0b 100644 --- a/lib/screens/repeater_settings_screen.dart +++ b/lib/screens/repeater_settings_screen.dart @@ -129,11 +129,22 @@ class _RepeaterSettingsScreenState extends State { _commandService?.handleResponse(widget.repeater, parsed.text); } + int _resolveRepeaterIndex = -1; + Contact _resolveRepeater(MeshCoreConnector connector) { - return connector.contacts.firstWhere( + if (_resolveRepeaterIndex >= 0 && + _resolveRepeaterIndex < connector.contacts.length && + connector.contacts[_resolveRepeaterIndex].publicKeyHex == + widget.repeater.publicKeyHex) { + return connector.contacts[_resolveRepeaterIndex]; + } + _resolveRepeaterIndex = connector.contacts.indexWhere( (c) => c.publicKeyHex == widget.repeater.publicKeyHex, - orElse: () => widget.repeater, ); + if (_resolveRepeaterIndex == -1) { + return widget.repeater; + } + return connector.contacts[_resolveRepeaterIndex]; } bool _matchesRepeaterPrefix(Uint8List prefix) { diff --git a/lib/screens/repeater_status_screen.dart b/lib/screens/repeater_status_screen.dart index 95254f4..f938419 100644 --- a/lib/screens/repeater_status_screen.dart +++ b/lib/screens/repeater_status_screen.dart @@ -91,11 +91,22 @@ class _RepeaterStatusScreenState extends State { }); } + int _resolveRepeaterIndex = -1; + Contact _resolveRepeater(MeshCoreConnector connector) { - return connector.contacts.firstWhere( + if (_resolveRepeaterIndex >= 0 && + _resolveRepeaterIndex < connector.contacts.length && + connector.contacts[_resolveRepeaterIndex].publicKeyHex == + widget.repeater.publicKeyHex) { + return connector.contacts[_resolveRepeaterIndex]; + } + _resolveRepeaterIndex = connector.contacts.indexWhere( (c) => c.publicKeyHex == widget.repeater.publicKeyHex, - orElse: () => widget.repeater, ); + if (_resolveRepeaterIndex == -1) { + return widget.repeater; + } + return connector.contacts[_resolveRepeaterIndex]; } void _handleTextMessageResponse(Uint8List frame) { diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index d6118f5..46d6352 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -287,10 +287,10 @@ class _SettingsScreenState extends State { const Divider(height: 1), ListTile( leading: const Icon(Icons.visibility_off_outlined), - title: Text(l10n.settings_privacyMode), - subtitle: Text(l10n.settings_privacyModeSubtitle), + title: Text(l10n.settings_privacy), + subtitle: Text(l10n.settings_privacySubtitle), trailing: const Icon(Icons.chevron_right), - onTap: () => _togglePrivacy(context, connector), + onTap: () => _privacySettings(context, connector), ), ], ), @@ -657,47 +657,6 @@ class _SettingsScreenState extends State { ); } - void _togglePrivacy(BuildContext context, MeshCoreConnector connector) { - final l10n = context.l10n; - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(l10n.settings_privacyMode), - content: Text(l10n.settings_privacyModeToggle), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(l10n.common_cancel), - ), - TextButton( - onPressed: () async { - Navigator.pop(context); - await connector.setPrivacyMode(true); - await connector.refreshDeviceInfo(); - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_privacyModeEnabled)), - ); - }, - child: Text(l10n.common_enable), - ), - TextButton( - onPressed: () async { - Navigator.pop(context); - await connector.setPrivacyMode(false); - await connector.refreshDeviceInfo(); - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_privacyModeDisabled)), - ); - }, - child: Text(l10n.common_disable), - ), - ], - ), - ); - } - void _sendAdvert(BuildContext context, MeshCoreConnector connector) { final l10n = context.l10n; connector.sendSelfAdvert(flood: true); @@ -977,6 +936,136 @@ class _SettingsScreenState extends State { } } +void _privacySettings(BuildContext context, MeshCoreConnector connector) { + final l10n = context.l10n; + + int telemetryMode = connector.telemetryModeBase; + int telemetryLocMode = connector.telemetryModeLoc; + int telemetryEnvMode = connector.telemetryModeEnv; + bool advertLocPolicy = connector.advertLocationPolicy == 0 ? false : true; + int multiAcks = connector.multiAcks; + + final telemModeBase = [ + DropdownMenuItem(value: teleModeDeny, child: Text(l10n.settings_denyAll)), + DropdownMenuItem( + value: teleModeAllowFlags, + child: Text(l10n.settings_allowByContact), + ), + DropdownMenuItem( + value: teleModeAllowAll, + child: Text(l10n.settings_allowAll), + ), + ]; + + showDialog( + context: context, + builder: (dialogContext) => StatefulBuilder( + builder: (context, setDialogState) => AlertDialog( + title: Text(l10n.settings_privacy), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.settings_privacySettingsDescription), + const SizedBox(height: 16), + FeatureToggleRow( + title: l10n.settings_advertLocation, + subtitle: l10n.settings_advertLocationSubtitle, + value: advertLocPolicy, + onChanged: (value) { + setDialogState(() => advertLocPolicy = value); + }, + ), + const SizedBox(height: 8), + DropdownButtonFormField( + initialValue: telemetryMode, + decoration: InputDecoration( + labelText: l10n.settings_telemetryBaseMode, + border: const OutlineInputBorder(), + ), + items: telemModeBase, + onChanged: (value) { + if (value != null) { + setDialogState(() => telemetryMode = value); + } + }, + ), + const SizedBox(height: 16), + DropdownButtonFormField( + initialValue: telemetryLocMode, + decoration: InputDecoration( + labelText: l10n.settings_telemetryLocationMode, + border: const OutlineInputBorder(), + ), + items: telemModeBase, + onChanged: (value) { + if (value != null) { + setDialogState(() => telemetryLocMode = value); + } + }, + ), + const SizedBox(height: 16), + DropdownButtonFormField( + initialValue: telemetryEnvMode, + decoration: InputDecoration( + labelText: l10n.settings_telemetryEnvironmentMode, + border: const OutlineInputBorder(), + ), + items: telemModeBase, + onChanged: (value) { + if (value != null) { + setDialogState(() => telemetryEnvMode = value); + } + }, + ), + const SizedBox(height: 16), + Text( + l10n.settings_multiAck(multiAcks.toString()), + style: Theme.of(context).textTheme.bodyMedium, + ), + Slider( + value: multiAcks.toDouble(), + min: 0, + max: 2, + divisions: 2, + label: multiAcks.toString(), + onChanged: (value) { + setDialogState(() => multiAcks = value.round()); + }, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(l10n.common_cancel), + ), + TextButton( + onPressed: () async { + Navigator.pop(context); + await connector.setTelemetryModeBase( + telemetryMode, + telemetryLocMode, + telemetryEnvMode, + advertLocPolicy ? 1 : 0, + multiAcks, + ); + await connector.refreshDeviceInfo(); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.settings_telemetryModeUpdated)), + ); + }, + child: Text(l10n.common_save), + ), + ], + ), + ), + ); +} + class _RadioSettingsDialog extends StatefulWidget { final MeshCoreConnector connector; diff --git a/lib/screens/telemetry_screen.dart b/lib/screens/telemetry_screen.dart index 3f95ccd..66911dc 100644 --- a/lib/screens/telemetry_screen.dart +++ b/lib/screens/telemetry_screen.dart @@ -10,30 +10,22 @@ import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../services/app_settings_service.dart'; import '../services/repeater_command_service.dart'; +import '../utils/app_logger.dart'; import '../widgets/path_management_dialog.dart'; import '../helpers/cayenne_lpp.dart'; import '../utils/battery_utils.dart'; class TelemetryScreen extends StatefulWidget { - final Contact repeater; - final String password; + final Contact contact; - const TelemetryScreen({ - super.key, - required this.repeater, - required this.password, - }); + const TelemetryScreen({super.key, required this.contact}); @override State createState() => _TelemetryScreenState(); } class _TelemetryScreenState extends State { - static const int _statusPayloadOffset = 8; - static const int _statusStatsSize = 52; - static const int _statusResponseBytes = - _statusPayloadOffset + _statusStatsSize; - Uint8List _tagData = Uint8List(4); + int _tagData = 0; bool _isLoading = false; bool _isLoaded = false; @@ -44,6 +36,26 @@ class _TelemetryScreenState extends State { PathSelection? _pendingStatusSelection; List>? _parsedTelemetry; + int _tripTime = 0; + + int _resolveContactIndex = -1; + + Contact _resolveContact(MeshCoreConnector connector) { + if (_resolveContactIndex >= 0 && + _resolveContactIndex < connector.contacts.length && + connector.contacts[_resolveContactIndex].publicKeyHex == + widget.contact.publicKeyHex) { + return connector.contacts[_resolveContactIndex]; + } + _resolveContactIndex = connector.contacts.indexWhere( + (c) => c.publicKeyHex == widget.contact.publicKeyHex, + ); + if (_resolveContactIndex == -1) { + return widget.contact; + } + return connector.contacts[_resolveContactIndex]; + } + @override void initState() { super.initState(); @@ -60,27 +72,62 @@ class _TelemetryScreenState extends State { // Listen for incoming text messages from the repeater _frameSubscription = connector.receivedFrames.listen((frame) { if (frame.isEmpty) return; + final reader = BufferReader(frame); + try { + final cmd = reader.readByte(); + if (cmd == respCodeSent) { + reader.skipBytes(1); // Skip the reserved byte + _tagData = reader.readUInt32LE(); + _tripTime = reader.readUInt32LE(); + _statusTimeout?.cancel(); + _statusTimeout = Timer(Duration(milliseconds: _tripTime), () { + if (!mounted) return; + setState(() { + _isLoading = false; + _isLoaded = false; + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.telemetry_requestTimeout), + backgroundColor: Colors.red, + ), + ); + _recordTelemetryResult(false); + }); + } - if (frame[0] == respCodeSent) { - _tagData = frame.sublist(2, 6); - } + // Check if it's a binary response + if (cmd == pushCodeBinaryResponse) { + if (!mounted) return; + reader.skipBytes(1); // Skip the reserved byte + if (reader.readUInt32LE() != _tagData) return; + _handleTelemetryResponse(reader.readRemainingBytes()); + } - // Check if it's a binary response - if (frame[0] == pushCodeBinaryResponse && - listEquals(frame.sublist(2, 6), _tagData)) { - if (!mounted) return; - _handleStatusResponse(frame.sublist(6)); + // Check if it's a telemetry response (for chat contacts) + if (cmd == pushCodeTelemetryResponse) { + reader.skipBytes(1); // Skip the reserved byte + final pubkey = reader.readBytes(6); + if (!mounted) return; + if (!listEquals(widget.contact.publicKey.sublist(0, 6), pubkey)) { + return; + } + _handleTelemetryResponse(reader.readRemainingBytes()); + } + } catch (e) { + appLogger.error('Error parsing incoming frame: $e'); + // If parsing fails, ignore the frame } }); } - void _handleStatusResponse(Uint8List frame) { + void _handleTelemetryResponse(Uint8List frame) { final parsedTelemetry = CayenneLpp.parseByChannel(frame); final batteryMv = _extractTelemetryBatteryMillivolts(parsedTelemetry); if (batteryMv != null) { final connector = Provider.of(context, listen: false); connector.updateRepeaterBatterySnapshot( - widget.repeater.publicKeyHex, + widget.contact.publicKeyHex, batteryMv, source: 'telemetry', ); @@ -105,13 +152,6 @@ class _TelemetryScreenState extends State { }); } - Contact _resolveRepeater(MeshCoreConnector connector) { - return connector.contacts.firstWhere( - (c) => c.publicKeyHex == widget.repeater.publicKeyHex, - orElse: () => widget.repeater, - ); - } - Future _loadTelemetry() async { if (_commandService == null) return; @@ -121,41 +161,20 @@ class _TelemetryScreenState extends State { }); try { final connector = Provider.of(context, listen: false); - final repeater = _resolveRepeater(connector); - final selection = await connector.preparePathForContactSend(repeater); + final selection = await connector.preparePathForContactSend( + _resolveContact(connector), + ); _pendingStatusSelection = selection; - final frame = buildSendBinaryReq( - repeater.publicKey, - payload: Uint8List.fromList([reqTypeGetTelemetry]), - ); - await connector.sendFrame(frame); - - final pathLengthValue = selection.useFlood ? -1 : selection.hopCount; - var messageBytes = frame.length >= _statusResponseBytes - ? frame.length - : _statusResponseBytes; - if (messageBytes < maxFrameSize) { - messageBytes = maxFrameSize; - } - final timeoutMs = connector.calculateTimeout( - pathLength: pathLengthValue, - messageBytes: messageBytes, - ); - _statusTimeout?.cancel(); - _statusTimeout = Timer(Duration(milliseconds: timeoutMs), () { - if (!mounted) return; - setState(() { - _isLoading = false; - _isLoaded = false; - }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.telemetry_requestTimeout), - backgroundColor: Colors.red, - ), + Uint8List frame; + if (widget.contact.type != advTypeChat) { + frame = buildSendBinaryReq( + widget.contact.publicKey, + payload: Uint8List.fromList([reqTypeGetTelemetry]), ); - _recordStatusResult(false); - }); + } else { + frame = buildSendTelemetryReq(widget.contact.publicKey); + } + await connector.sendFrame(frame); } catch (e) { if (mounted) { setState(() { @@ -173,12 +192,16 @@ class _TelemetryScreenState extends State { } } - void _recordStatusResult(bool success) { + void _recordTelemetryResult(bool success) { final selection = _pendingStatusSelection; if (selection == null) return; final connector = Provider.of(context, listen: false); - final repeater = _resolveRepeater(connector); - connector.recordRepeaterPathResult(repeater, selection, success, null); + connector.recordRepeaterPathResult( + widget.contact, + selection, + success, + null, + ); _pendingStatusSelection = null; } @@ -196,8 +219,7 @@ class _TelemetryScreenState extends State { final connector = context.watch(); final settings = context.watch().settings; final isImperialUnits = settings.unitSystem == UnitSystem.imperial; - final repeater = _resolveRepeater(connector); - final isFloodMode = repeater.pathOverride == -1; + final isFloodMode = widget.contact.pathOverride == -1; return Scaffold( appBar: AppBar( @@ -210,7 +232,7 @@ class _TelemetryScreenState extends State { style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), Text( - repeater.name, + widget.contact.name, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.normal, @@ -225,9 +247,9 @@ class _TelemetryScreenState extends State { tooltip: l10n.repeater_routingMode, onSelected: (mode) async { if (mode == 'flood') { - await connector.setPathOverride(repeater, pathLen: -1); + await connector.setPathOverride(widget.contact, pathLen: -1); } else { - await connector.setPathOverride(repeater, pathLen: null); + await connector.setPathOverride(widget.contact, pathLen: null); } }, itemBuilder: (context) => [ @@ -283,7 +305,7 @@ class _TelemetryScreenState extends State { icon: const Icon(Icons.timeline), tooltip: l10n.repeater_pathManagement, onPressed: () => - PathManagementDialog.show(context, contact: repeater), + PathManagementDialog.show(context, contact: widget.contact), ), IconButton( icon: _isLoading @@ -437,7 +459,7 @@ class _TelemetryScreenState extends State { final l10n = context.l10n; final connector = context.watch(); final batteryMv = - connector.getRepeaterBatteryMillivolts(widget.repeater.publicKeyHex) ?? + connector.getRepeaterBatteryMillivolts(widget.contact.publicKeyHex) ?? (telemetryVolts == null ? null : (telemetryVolts * 1000).round()); if (batteryMv == null) return l10n.common_notAvailable; final chemistry = _batteryChemistry(); @@ -449,7 +471,7 @@ class _TelemetryScreenState extends State { String _batteryChemistry() { final settingsService = context.read(); return settingsService.batteryChemistryForRepeater( - widget.repeater.publicKeyHex, + widget.contact.publicKeyHex, ); } diff --git a/lib/services/message_retry_service.dart b/lib/services/message_retry_service.dart index b284425..c8e89aa 100644 --- a/lib/services/message_retry_service.dart +++ b/lib/services/message_retry_service.dart @@ -11,7 +11,7 @@ import 'app_debug_log_service.dart'; class _AckHistoryEntry { final String messageId; - final List ackHashes; + final List ackHashes; final DateTime timestamp; _AckHistoryEntry({ @@ -77,7 +77,7 @@ class MessageRetryService extends ChangeNotifier { final Map _pendingContacts = {}; final Map> _attemptPathHistory = {}; final Map _ackHashToMessageId = {}; - final Map> _expectedAckHashes = {}; + final Map> _expectedAckHashes = {}; final List<_AckHistoryEntry> _ackHistory = []; final Map> _sendQueue = {}; final Set _activeMessages = {}; @@ -98,7 +98,7 @@ class MessageRetryService extends ChangeNotifier { /// Compute expected ACK hash using same algorithm as firmware: /// SHA256([timestamp(4)][attempt(1)][text][sender_pubkey(32)]) -> first 4 bytes - static Uint8List computeExpectedAckHash( + static int computeExpectedAckHash( int timestampSeconds, int attempt, String text, @@ -126,7 +126,8 @@ class MessageRetryService extends ChangeNotifier { // Compute SHA256 and return first 4 bytes final hash = sha256.convert(buffer); - return Uint8List.fromList(hash.bytes.sublist(0, 4)); + final bytes = Uint8List.fromList(hash.bytes.sublist(0, 4)); + return (bytes[3] << 24) | (bytes[2] << 16) | (bytes[1] << 8) | bytes[0]; } Future sendMessageWithRetry({ @@ -324,9 +325,7 @@ class MessageRetryService extends ChangeNotifier { outboundText, selfPubKey, ); - final expectedHashHex = expectedHash - .map((b) => b.toRadixString(16).padLeft(2, '0')) - .join(); + final expectedHashHex = expectedHash.toRadixString(16).padLeft(8, '0'); _expectedHashToMessageId[expectedHashHex] = messageId; final shortText = message.text.length > 20 @@ -341,13 +340,11 @@ class MessageRetryService extends ChangeNotifier { config.sendMessage(contact, message.text, attempt, timestampSeconds); } - bool updateMessageFromSent(Uint8List ackHash, int timeoutMs) { + bool updateMessageFromSent(int ackHash, int timeoutMs) { final config = _config; if (config == null) return false; - final ackHashHex = ackHash - .map((b) => b.toRadixString(16).padLeft(2, '0')) - .join(); + final ackHashHex = ackHash.toRadixString(16).padLeft(8, '0'); // Try hash-based matching (fixes LoRa message drops causing mismatches) String? messageId = _expectedHashToMessageId.remove(ackHashHex); @@ -389,10 +386,8 @@ class MessageRetryService extends ChangeNotifier { // Add this ACK hash to the list of expected ACKs for this message (for history) _expectedAckHashes[messageId] ??= []; - if (!_expectedAckHashes[messageId]!.any( - (hash) => listEquals(hash, ackHash), - )) { - _expectedAckHashes[messageId]!.add(Uint8List.fromList(ackHash)); + if (!_expectedAckHashes[messageId]!.any((hash) => hash == ackHash)) { + _expectedAckHashes[messageId]!.add(ackHash); } // Calculate timeout: prefer ML prediction, then device-provided, then physics fallback @@ -559,10 +554,10 @@ class MessageRetryService extends ChangeNotifier { } } - bool _checkAckHistory(Uint8List ackHash) { + bool _checkAckHistory(int ackHash) { for (final entry in _ackHistory) { for (final expectedHash in entry.ackHashes) { - if (listEquals(expectedHash, ackHash)) { + if (expectedHash == ackHash) { return true; } } @@ -570,13 +565,11 @@ class MessageRetryService extends ChangeNotifier { return false; } - void handleAckReceived(Uint8List ackHash, int tripTimeMs) { + void handleAckReceived(int ackHash, int tripTimeMs) { final config = _config; String? matchedMessageId; int? matchedAttemptIndex; - final ackHashHex = ackHash - .map((b) => b.toRadixString(16).padLeft(2, '0')) - .join(); + final ackHashHex = ackHash.toRadixString(16).padLeft(8, '0'); // Clean up old ACK hash mappings (older than 15 minutes) final cutoffTime = DateTime.now().subtract(const Duration(minutes: 15)); @@ -606,7 +599,7 @@ class MessageRetryService extends ChangeNotifier { final expectedHashes = entry.value; for (final expectedHash in expectedHashes) { - if (listEquals(expectedHash, ackHash)) { + if (expectedHash == ackHash) { matchedMessageId = messageId; matchedAttemptIndex = expectedHashes.indexOf(expectedHash); break; @@ -685,11 +678,11 @@ class MessageRetryService extends ChangeNotifier { } } - String? getContactKeyForAckHash(Uint8List ackHash) { + String? getContactKeyForAckHash(int ackHash) { for (var entry in _pendingMessages.entries) { final message = entry.value; if (message.expectedAckHash != null && - listEquals(message.expectedAckHash, ackHash)) { + message.expectedAckHash == ackHash) { final contact = _pendingContacts[entry.key]; return contact?.publicKeyHex; } diff --git a/lib/storage/message_store.dart b/lib/storage/message_store.dart index 44d3621..5550911 100644 --- a/lib/storage/message_store.dart +++ b/lib/storage/message_store.dart @@ -85,9 +85,7 @@ class MessageStore { 'messageId': msg.messageId, 'retryCount': msg.retryCount, 'estimatedTimeoutMs': msg.estimatedTimeoutMs, - 'expectedAckHash': msg.expectedAckHash != null - ? base64Encode(msg.expectedAckHash!) - : null, + 'expectedAckHash': msg.expectedAckHash, 'sentAt': msg.sentAt?.millisecondsSinceEpoch, 'deliveredAt': msg.deliveredAt?.millisecondsSinceEpoch, 'tripTimeMs': msg.tripTimeMs, @@ -119,9 +117,7 @@ class MessageStore { messageId: json['messageId'] as String?, retryCount: json['retryCount'] as int? ?? 0, estimatedTimeoutMs: json['estimatedTimeoutMs'] as int?, - expectedAckHash: json['expectedAckHash'] != null - ? Uint8List.fromList(base64Decode(json['expectedAckHash'] as String)) - : null, + expectedAckHash: json['expectedAckHash'] as int? ?? 0, sentAt: json['sentAt'] != null ? DateTime.fromMillisecondsSinceEpoch(json['sentAt'] as int) : null, diff --git a/lib/widgets/path_management_dialog.dart b/lib/widgets/path_management_dialog.dart index f667256..e92f301 100644 --- a/lib/widgets/path_management_dialog.dart +++ b/lib/widgets/path_management_dialog.dart @@ -34,11 +34,22 @@ class _PathManagementDialog extends StatefulWidget { class _PathManagementDialogState extends State<_PathManagementDialog> { bool _showAllPaths = false; + int _resolveContactIndex = -1; + Contact _resolveContact(MeshCoreConnector connector) { - return connector.contacts.firstWhere( + if (_resolveContactIndex >= 0 && + _resolveContactIndex < connector.contacts.length && + connector.contacts[_resolveContactIndex].publicKeyHex == + widget.contact.publicKeyHex) { + return connector.contacts[_resolveContactIndex]; + } + _resolveContactIndex = connector.contacts.indexWhere( (c) => c.publicKeyHex == widget.contact.publicKeyHex, - orElse: () => widget.contact, ); + if (_resolveContactIndex == -1) { + return widget.contact; + } + return connector.contacts[_resolveContactIndex]; } String _formatRelativeTime(BuildContext context, DateTime? time) { diff --git a/lib/widgets/repeater_login_dialog.dart b/lib/widgets/repeater_login_dialog.dart index ec0af66..ce6c2b7 100644 --- a/lib/widgets/repeater_login_dialog.dart +++ b/lib/widgets/repeater_login_dialog.dart @@ -69,11 +69,21 @@ class _RepeaterLoginDialogState extends State { bool _isLoggingIn = false; + int _resolveRepeaterIndex = -1; Contact _resolveRepeater(MeshCoreConnector connector) { - return connector.contacts.firstWhere( + if (_resolveRepeaterIndex >= 0 && + _resolveRepeaterIndex < connector.contacts.length && + connector.contacts[_resolveRepeaterIndex].publicKeyHex == + widget.repeater.publicKeyHex) { + return connector.contacts[_resolveRepeaterIndex]; + } + _resolveRepeaterIndex = connector.contacts.indexWhere( (c) => c.publicKeyHex == widget.repeater.publicKeyHex, - orElse: () => widget.repeater, ); + if (_resolveRepeaterIndex == -1) { + return widget.repeater; + } + return connector.contacts[_resolveRepeaterIndex]; } Future _handleLogin() async { diff --git a/lib/widgets/room_login_dialog.dart b/lib/widgets/room_login_dialog.dart index 7324f44..91d2c8c 100644 --- a/lib/widgets/room_login_dialog.dart +++ b/lib/widgets/room_login_dialog.dart @@ -64,11 +64,22 @@ class _RoomLoginDialogState extends State { bool _isLoggingIn = false; + int _resolveRepeaterIndex = -1; + Contact _resolveRepeater(MeshCoreConnector connector) { - return connector.contacts.firstWhere( + if (_resolveRepeaterIndex >= 0 && + _resolveRepeaterIndex < connector.contacts.length && + connector.contacts[_resolveRepeaterIndex].publicKeyHex == + widget.room.publicKeyHex) { + return connector.contacts[_resolveRepeaterIndex]; + } + _resolveRepeaterIndex = connector.contacts.indexWhere( (c) => c.publicKeyHex == widget.room.publicKeyHex, - orElse: () => widget.room, ); + if (_resolveRepeaterIndex == -1) { + return widget.room; + } + return connector.contacts[_resolveRepeaterIndex]; } Future _handleLogin() async { diff --git a/test/services/retry_and_protocol_test.dart b/test/services/retry_and_protocol_test.dart index b58da45..b6c53b6 100644 --- a/test/services/retry_and_protocol_test.dart +++ b/test/services/retry_and_protocol_test.dart @@ -14,7 +14,7 @@ import 'package:meshcore_open/services/message_retry_service.dart'; /// Replicates the SHA-256 computation from [MessageRetryService.computeExpectedAckHash] /// so tests can cross-check without calling the real implementation twice. -Uint8List _manualAckHash( +int _manualAckHash( int timestampSeconds, int attemptMasked, // already masked to 0x03 String text, @@ -35,7 +35,8 @@ Uint8List _manualAckHash( buffer.setRange(offset, offset + senderPubKey.length, senderPubKey); final hash = sha256.convert(buffer); - return Uint8List.fromList(hash.bytes.sublist(0, 4)); + final bytes = Uint8List.fromList(hash.bytes.sublist(0, 4)); + return (bytes[3] << 24) | (bytes[2] << 16) | (bytes[1] << 8) | bytes[0]; } Uint8List _makeKey(int seed) { @@ -169,16 +170,6 @@ void main() { expect(first, equals(second)); }); - test('hash is exactly 4 bytes long', () { - final hash = MessageRetryService.computeExpectedAckHash( - fixedTs, - 0, - fixedText, - fixedKey, - ); - expect(hash.length, equals(4)); - }); - test('hash matches manual SHA-256 computation', () { for (int attempt = 0; attempt < 4; attempt++) { final actual = MessageRetryService.computeExpectedAckHash( @@ -509,7 +500,7 @@ void main() { fixedText, fixedKey, ); - final hex = hash.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); + final hex = hash.toRadixString(16).padLeft(8, '0'); expect( hashes.containsKey(hex), isFalse,