diff --git a/README.md b/README.md index 10fb0a5..3b230dd 100644 --- a/README.md +++ b/README.md @@ -231,6 +231,11 @@ If you find MeshCore Open useful and would like to support development, you can **Solana Address:** `F15YanjZj96YTBtKJYgNa8RLQLCZkx5CEwogPWkqXeoQ` + +**Monero Address:** `453TxnpUqjkJtXxzdjMsrgERNkBRXEGamPbpC45ENrvKAk9tH7kZbxWF82Hz66etgDZyXFPEBU2JUEqhLeJyWt9kBvTVy5m` + +**Bitcoin Address:** `bc1qh45x28v8dslcg4v4upmqd9g0mvc3lnyffmyzr5` + Your support helps maintain and improve this open-source project! ## Acknowledgments diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 95bce3b..71418a4 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:crypto/crypto.dart' as crypto; +import 'package:meshcore_open/models/discovery_contact.dart'; import 'package:pointycastle/export.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; @@ -25,6 +26,7 @@ import '../storage/channel_message_store.dart'; import '../storage/channel_order_store.dart'; import '../storage/channel_settings_store.dart'; import '../storage/channel_store.dart'; +import '../storage/contact_discovery_store.dart'; import '../storage/contact_settings_store.dart'; import '../storage/contact_store.dart'; import '../storage/message_store.dart'; @@ -120,6 +122,7 @@ class MeshCoreConnector extends ChangeNotifier { final List _scanResults = []; final List _contacts = []; + final List _discoveredContacts = []; final List _channels = []; final Map> _conversations = {}; final Map> _channelMessages = {}; @@ -136,10 +139,13 @@ class MeshCoreConnector extends ChangeNotifier { StreamSubscription>? _scanSubscription; StreamSubscription? _connectionSubscription; StreamSubscription>? _notifySubscription; + Timer? _notifyListenersTimer; Timer? _selfInfoRetryTimer; Timer? _reconnectTimer; Timer? _batteryPollTimer; int _reconnectAttempts = 0; + bool _notifyListenersDirty = false; + static const Duration _notifyListenersDebounce = Duration(milliseconds: 50); final StreamController _receivedFramesController = StreamController.broadcast(); @@ -170,6 +176,18 @@ class MeshCoreConnector extends ChangeNotifier { bool _pendingDeferredChannelSyncAfterContacts = false; bool _webInitialHandshakeRequestSent = false; bool _preserveContactsOnRefresh = false; + bool _autoAddUsers = false; + bool _autoAddRepeaters = false; + bool _autoAddRoomServers = false; + bool _autoAddSensors = false; + bool _overwriteOldest = false; + bool _manualAddContacts = false; + int _telemetryModeBase = 0; + int _telemetryModeLoc = 0; + int _telemetryModeEnv = 0; + int _advertLocPolicy = 0; + int _multiAcks = 0; + static const int _defaultMaxContacts = 32; static const int _defaultMaxChannels = 8; int _maxContacts = _defaultMaxContacts; @@ -210,6 +228,7 @@ class MeshCoreConnector extends ChangeNotifier { final ChannelSettingsStore _channelSettingsStore = ChannelSettingsStore(); final ContactSettingsStore _contactSettingsStore = ContactSettingsStore(); final ContactStore _contactStore = ContactStore(); + final ContactDiscoveryStore _discoveryContactStore = ContactDiscoveryStore(); final ChannelStore _channelStore = ChannelStore(); final UnreadStore _unreadStore = UnreadStore(); List _cachedChannels = []; @@ -265,6 +284,10 @@ class MeshCoreConnector extends ChangeNotifier { ); } + List get discoveredContacts { + return List.unmodifiable(_discoveredContacts); + } + List get channels => List.unmodifiable(_channels); bool get isConnected => _state == MeshCoreConnectionState.connected; bool get isLoadingContacts => _isLoadingContacts; @@ -281,12 +304,18 @@ class MeshCoreConnector extends ChangeNotifier { int? get currentBwHz => _currentBwHz; int? get currentSf => _currentSf; int? get currentCr => _currentCr; + bool? get autoAddUsers => _autoAddUsers; + bool? get autoAddRepeaters => _autoAddRepeaters; + bool? get autoAddRoomServers => _autoAddRoomServers; + bool? get autoAddSensors => _autoAddSensors; + bool? get autoAddOverwriteOldest => _overwriteOldest; bool? get clientRepeat => _clientRepeat; int? get firmwareVerCode => _firmwareVerCode; Map? get currentCustomVars => _currentCustomVars; int? get batteryMillivolts => _batteryMillivolts; int get maxContacts => _maxContacts; int get maxChannels => _maxChannels; + Set get knownContactKeys => Set.unmodifiable(_knownContactKeys); bool get isSyncingQueuedMessages => _isSyncingQueuedMessages; bool get isSyncingChannels => _isSyncingChannels; int get channelSyncProgress => @@ -655,6 +684,13 @@ class MeshCoreConnector extends ChangeNotifier { } } + Future loadDiscoveredContactCache() async { + final cached = await _discoveryContactStore.loadContacts(); + _discoveredContacts + ..clear() + ..addAll(cached); + } + Future loadChannelSettings({int? maxChannels}) async { _channelSmazEnabled.clear(); final channelCount = maxChannels ?? _maxChannels; @@ -1083,7 +1119,7 @@ class MeshCoreConnector extends ChangeNotifier { _hasReceivedDeviceInfo = false; _pendingInitialChannelSync = true; } - unawaited(Future.microtask(() => _startBleInitialSync())); + await _startBleInitialSync(); } catch (e) { _appDebugLogService?.error('Connection error: $e', tag: 'BLE Connect'); await disconnect(manual: false); @@ -1132,22 +1168,7 @@ class MeshCoreConnector extends ChangeNotifier { await _requestDeviceInfo(); _startBatteryPolling(); - - if (PlatformInfo.isWeb) { - // Keep Web BLE startup writes light while notifications settle. - unawaited( - Future(() async { - await Future.delayed(const Duration(seconds: 5)); - if (!isConnected || - !PlatformInfo.isWeb || - _activeTransport != MeshCoreTransportType.bluetooth) { - return; - } - await syncTime(); - }), - ); - return; - } + unawaited(loadDiscoveredContactCache()); final gotSelfInfo = await _waitForSelfInfo( timeout: const Duration(seconds: 3), @@ -1157,8 +1178,8 @@ class MeshCoreConnector extends ChangeNotifier { await _waitForSelfInfo(timeout: const Duration(seconds: 3)); } - unawaited(syncTime()); - _pendingDeferredChannelSyncAfterContacts = true; + await syncTime(); + unawaited(getChannels()); } void _resetConnectionHandshakeState() { @@ -1280,6 +1301,7 @@ class MeshCoreConnector extends ChangeNotifier { _deviceDisplayName = null; _deviceId = null; _contacts.clear(); + _discoveredContacts.clear(); _conversations.clear(); _loadedConversationKeys.clear(); _selfPublicKey = null; @@ -1397,8 +1419,9 @@ class MeshCoreConnector extends ChangeNotifier { await sendFrame(buildDeviceQueryFrame()); await sendFrame(buildAppStartFrame()); await requestBatteryStatus(force: true); - await sendFrame(buildGetRadioSettingsFrame()); await sendFrame(buildGetCustomVarsFrame()); + await sendFrame(buildGetAutoAddFlagsFrame()); + _scheduleSelfInfoRetry(); } @@ -1420,7 +1443,7 @@ class MeshCoreConnector extends ChangeNotifier { await sendFrame(buildAppStartFrame()); await sendFrame(buildGetCustomVarsFrame()); await requestBatteryStatus(); - + await sendFrame(buildGetAutoAddFlagsFrame()); _scheduleSelfInfoRetry(); } @@ -1857,6 +1880,8 @@ class MeshCoreConnector extends ChangeNotifier { Future removeContact(Contact contact) async { if (!isConnected) return; + _handleDiscovery(contact, Uint8List(0), noNotify: true); + await sendFrame(buildRemoveContactFrame(contact.publicKey)); _contacts.removeWhere((c) => c.publicKeyHex == contact.publicKeyHex); _knownContactKeys.remove(contact.publicKeyHex); @@ -1871,6 +1896,42 @@ class MeshCoreConnector extends ChangeNotifier { notifyListeners(); } + Future removeDiscoveredContact(DiscoveryContact contact) async { + if (!isConnected) return; + _discoveredContacts.removeWhere( + (c) => c.publicKeyHex == contact.publicKeyHex, + ); + unawaited(_persistDiscoveredContacts()); + notifyListeners(); + } + + Future importDiscoveredContact(DiscoveryContact contact) async { + if (!isConnected) return; + + await sendFrame( + buildUpdateContactPathFrame( + contact.publicKey, + contact.path, + contact.pathLength, + type: contact.type, + flags: 0, + name: contact.name, + ), + ); + + _handleContactAdvert( + Contact( + publicKey: contact.publicKey, + name: contact.name, + type: contact.type, + pathLength: contact.pathLength, + path: contact.path, + lastSeen: DateTime.now(), + ), + ); + notifyListeners(); + } + Future clearContactPath(Contact contact) async { if (!isConnected) return; @@ -2284,8 +2345,9 @@ class MeshCoreConnector extends ChangeNotifier { case respCodeChannelInfo: _handleChannelInfo(frame); break; - case respCodeRadioSettings: - _handleRadioSettings(frame); + case respCodeAutoAddConfig: + _handleAutoAddConfig(frame); + _checkManualAddContacts(); break; case respCodeBattAndStorage: _handleBatteryAndStorage(frame); @@ -2358,27 +2420,35 @@ class MeshCoreConnector extends ChangeNotifier { // [56] = sf // [57] = cr // [58+] = node_name - if (frame.length < 4 + pubKeySize) return; - final wasAwaitingSelfInfo = _awaitingSelfInfo; + final reader = BufferReader(frame); + try { + reader.skipBytes(2); + _currentTxPower = reader.readByte(); + _maxTxPower = reader.readByte(); + _selfPublicKey = reader.readBytes(pubKeySize); + _selfLatitude = reader.readInt32LE() / 1000000.0; + _selfLongitude = reader.readInt32LE() / 1000000.0; + _multiAcks = reader.readByte(); + _advertLocPolicy = reader.readByte(); + final telemetryFlag = reader.readByte(); + _telemetryModeBase = telemetryFlag & 0x03; + _telemetryModeEnv = telemetryFlag >> 2 & 0x03; + _telemetryModeLoc = telemetryFlag >> 4 & 0x03; - _currentTxPower = frame[2]; - _maxTxPower = frame[3]; - _selfPublicKey = Uint8List.fromList(frame.sublist(4, 4 + pubKeySize)); - _selfLatitude = readInt32LE(frame, 36) / 1000000.0; - _selfLongitude = readInt32LE(frame, 40) / 1000000.0; + _manualAddContacts = reader.readByte() & 0x01 == 0x00; - // Radio settings (if frame is long enough) - if (frame.length >= 58) { - _currentFreqHz = readUint32LE(frame, 48); - _currentBwHz = readUint32LE(frame, 52); - _currentSf = frame[56]; - _currentCr = frame[57]; - } + _currentFreqHz = reader.readUInt32LE(); + _currentBwHz = reader.readUInt32LE(); + _currentSf = reader.readByte(); + _currentCr = reader.readByte(); - // Node name starts at offset 58 if frame is long enough - if (frame.length > 58) { - _selfName = readCString(frame, 58, frame.length - 58); + _selfName = reader.readString(); + } catch (e) { + _appDebugLogService?.error( + 'Error parsing SELF_INFO frame: $e', + tag: 'Connector', + ); } final selfName = _selfName?.trim(); if (_activeTransport == MeshCoreTransportType.usb && @@ -2486,25 +2556,6 @@ class MeshCoreConnector extends ChangeNotifier { unawaited(_requestNextQueuedMessage()); } - void _handleRadioSettings(Uint8List frame) { - // Frame format from C++: - // [0] = RESP_CODE_RADIO_SETTINGS - // [1-4] = freq (uint32 LE, in Hz) - // [5-8] = bw (uint32 LE, in Hz) - // [9] = sf - // [10] = cr - if (frame.length >= 11) { - _currentFreqHz = readUint32LE(frame, 1); - _currentBwHz = readUint32LE(frame, 5); - _currentSf = frame[9]; - _currentCr = frame[10]; - debugPrint( - 'Radio settings: freq=$_currentFreqHz bw=$_currentBwHz sf=$_currentSf cr=$_currentCr', - ); - notifyListeners(); - } - } - void _handleBatteryAndStorage(Uint8List frame) { // Frame format from C++: // [0] = RESP_CODE_BATT_AND_STORAGE @@ -2522,6 +2573,32 @@ class MeshCoreConnector extends ChangeNotifier { } } + void _checkManualAddContacts() async { + // If manual add contacts is enabled, set auto add config and other params. + // and disable it after + if (_manualAddContacts) { + await sendFrame( + buildSetAutoAddConfigFrame( + autoAddChat: true, + autoAddRepeater: true, + autoAddRoomServer: true, + autoAddSensor: true, + overwriteOldest: _overwriteOldest, + ), + ); + await sendFrame( + buildSetOtherParamsFrame( + (_telemetryModeEnv << 4) | + (_telemetryModeLoc << 2) | + (_telemetryModeBase), + _advertLocPolicy, + _multiAcks, + ), + ); + _manualAddContacts = false; + } + } + /// Calculate timeout for a message based on radio settings and path length /// Returns timeout in milliseconds, considering number of hops int calculateTimeout({required int pathLength, int messageBytes = 100}) { @@ -2705,6 +2782,10 @@ class MeshCoreConnector extends ChangeNotifier { await _contactStore.saveContacts(_contacts); } + Future _persistDiscoveredContacts() async { + await _discoveryContactStore.saveContacts(_discoveredContacts); + } + int _latestContactLastmod() { if (_contacts.isEmpty) return 0; var latest = 0; @@ -4111,12 +4192,47 @@ class MeshCoreConnector extends ChangeNotifier { } } + void markNotifyDirty() { + if (_notifyListenersDirty && _notifyListenersTimer != null) { + return; + } + + _notifyListenersDirty = true; + _notifyListenersTimer ??= Timer( + _notifyListenersDebounce, + _flushBatchedNotify, + ); + } + + void _flushBatchedNotify() { + _notifyListenersTimer = null; + if (!_notifyListenersDirty) { + return; + } + + _notifyListenersDirty = false; + super.notifyListeners(); + + if (_notifyListenersDirty && _notifyListenersTimer == null) { + _notifyListenersTimer = Timer( + _notifyListenersDebounce, + _flushBatchedNotify, + ); + } + } + + @override + void notifyListeners() { + markNotifyDirty(); + } + @override void dispose() { _scanSubscription?.cancel(); _connectionSubscription?.cancel(); _usbFrameSubscription?.cancel(); _notifySubscription?.cancel(); + _notifyListenersTimer?.cancel(); _reconnectTimer?.cancel(); _batteryPollTimer?.cancel(); _receivedFramesController.close(); @@ -4151,22 +4267,99 @@ class MeshCoreConnector extends ChangeNotifier { appLogger.warn('Malformed RX frame: $e', tag: 'Connector'); return; } - + final rawPacket = frame.sublist(3); switch (payloadType) { case payloadTypeADVERT: - _handlePayloadAdvertReceived(payload, pathBytes, routeType, snr); + _handlePayloadAdvertReceived( + rawPacket, + payload, + pathBytes, + routeType, + snr, + ); break; default: } } + void importContact(Uint8List frame) { + final packet = BufferReader(frame); + int payloadType = 0; + Uint8List pathBytes = Uint8List(0); + try { + packet.skipBytes(1); // Skip frame type byte + packet.skipBytes(1); // Skip SNR byte + packet.skipBytes(1); // Skip RSSI byte + final header = packet.readByte(); + payloadType = (header >> 2) & 0x0F; + //final payloadVer = (header >> 6) & 0x03; + final pathLen = packet.readByte(); + pathBytes = packet.readBytes(pathLen); + } catch (e) { + appLogger.warn('Malformed RX frame: $e', tag: 'Connector'); + return; + } + double latitude = 0.0; + double longitude = 0.0; + String name = ''; + Uint8List publicKey = Uint8List(0); + int type = 0; + int timestamp = 0; + bool hasLocation = false; + bool hasName = false; + if (payloadType != payloadTypeADVERT) { + appLogger.warn('Unexpected payload type: $payloadType', tag: 'Connector'); + return; + } + try { + publicKey = packet.readBytes(32); + timestamp = packet.readInt32LE(); + //TODO add signature verification + packet.skipBytes(64); // Skip signature for now + final flags = packet.readByte(); + type = flags & 0x0F; + hasLocation = (flags & 0x10) != 0; + // For future use: + //final hasFeature1 = (flags & 0x20) != 0; + //final hasFeature2 = (flags & 0x40) != 0; + hasName = (flags & 0x80) != 0; + if (hasLocation && packet.remaining >= 8) { + latitude = packet.readInt32LE() / 1e6; + longitude = packet.readInt32LE() / 1e6; + } + if (hasName && packet.remaining > 0) { + name = packet.readString(); + } + } catch (e) { + appLogger.warn('Malformed advert frame: $e', tag: 'Connector'); + return; + } + + importDiscoveredContact( + DiscoveryContact( + rawPacket: frame, + publicKey: publicKey, + name: name, + type: type, + pathLength: pathBytes.length, + path: Uint8List.fromList( + pathBytes.reversed.toList(), + ), // Store path in reverse for easier use in outgoing messages + latitude: latitude, + longitude: longitude, + lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), + ), + ); + } + void _handlePayloadAdvertReceived( - Uint8List frame, + Uint8List rawPacket, + Uint8List payload, Uint8List path, int routeType, double snr, ) { - final advert = BufferReader(frame); + final advert = BufferReader(payload); double latitude = 0.0; double longitude = 0.0; String name = ''; @@ -4204,6 +4397,7 @@ class MeshCoreConnector extends ChangeNotifier { return; } + //We ignore our own adverts if (listEquals(publicKey, _selfPublicKey)) { return; } @@ -4224,7 +4418,14 @@ class MeshCoreConnector extends ChangeNotifier { longitude: longitude, lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), ); - _handleContactAdvert(newContact); + if ((_autoAddUsers && type == advTypeChat) || + (_autoAddRepeaters && type == advTypeRepeater) || + (_autoAddRoomServers && type == advTypeRoom) || + (_autoAddSensors && type == advTypeSensor)) { + _handleContactAdvert(newContact); + } else { + _handleDiscovery(newContact, rawPacket); + } _updateDirectRepeater(newContact, snr, path); return; } @@ -4312,6 +4513,84 @@ class MeshCoreConnector extends ChangeNotifier { } notifyListeners(); } + + void _handleAutoAddConfig(Uint8List frame) { + final reader = BufferReader(frame); + try { + reader.skipBytes(1); // Skip the response code byte + final flags = reader.readByte(); + _autoAddUsers = flags & autoAddChatFlag != 0; + _autoAddRepeaters = flags & autoAddRepeaterFlag != 0; + _autoAddRoomServers = flags & autoAddRoomServerFlag != 0; + _autoAddSensors = flags & autoAddSensorFlag != 0; + _overwriteOldest = flags & autoAddOverwriteOldestFlag != 0; + } catch (e) { + appLogger.error('Failed to parse auto-add config: $e', tag: 'Connector'); + } + } + + void _handleDiscovery( + Contact contact, + Uint8List rawPacket, { + bool noNotify = false, + }) { + appLogger.info('Discovered new contact: ${contact.name}', tag: 'Connector'); + + final existingIndex = _discoveredContacts.indexWhere( + (c) => c.publicKeyHex == contact.publicKeyHex, + ); + + // Update existing contact + if (existingIndex >= 0) { + _discoveredContacts[existingIndex] = _discoveredContacts[existingIndex] + .copyWith( + rawPacket: rawPacket, + name: contact.name, + type: contact.type, + pathLength: contact.pathLength, + path: contact.path, + latitude: contact.latitude, + longitude: contact.longitude, + lastSeen: contact.lastSeen, + ); + notifyListeners(); + unawaited(_persistDiscoveredContacts()); + return; + } + + final disContact = DiscoveryContact( + rawPacket: rawPacket, + publicKey: contact.publicKey, + name: contact.name, + type: contact.type, + pathLength: contact.pathLength, + path: contact.path, + latitude: contact.latitude, + longitude: contact.longitude, + lastSeen: contact.lastSeen, + ); + _discoveredContacts.add(disContact); + + unawaited(_persistDiscoveredContacts()); + + // Show notification for new contact (advertisement) + if (_appSettingsService != null && !noNotify) { + final settings = _appSettingsService!.settings; + if (settings.notificationsEnabled && settings.notifyOnNewAdvert) { + _notificationService.showAdvertNotification( + contactName: contact.name, + contactType: contact.typeLabel, + contactId: contact.publicKeyHex, + ); + } + } + } + + void removeAllDiscoveredContacts() { + _discoveredContacts.clear(); + unawaited(_persistDiscoveredContacts()); + notifyListeners(); + } } const int _phRouteMask = 0x03; diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 938a274..58abf6f 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -120,25 +120,27 @@ class BufferWriter { } void writeHex(String hex) { - // Validate hex string length is even and not empty - if (hex.isEmpty || hex.length % 2 != 0) { - throw FormatException('Invalid hex string length: ${hex.length}'); - } - List result = []; - for (int i = 0; i < hex.length ~/ 2; i++) { - final hexByte = hex.substring(i * 2, i * 2 + 2); - final byte = int.tryParse(hexByte, radix: 16); - if (byte == null) { - throw FormatException( - 'Invalid hex characters at position $i: $hexByte', - ); - } - result.add(byte); - } - writeBytes(Uint8List.fromList(result)); + writeBytes(hex2Uint8List(hex)); } } +Uint8List hex2Uint8List(String hex) { + // Validate hex string length is even and not empty + if (hex.isEmpty || hex.length % 2 != 0) { + throw FormatException('Invalid hex string length: ${hex.length}'); + } + List result = []; + for (int i = 0; i < hex.length ~/ 2; i++) { + final hexByte = hex.substring(i * 2, i * 2 + 2); + final byte = int.tryParse(hexByte, radix: 16); + if (byte == null) { + throw FormatException('Invalid hex characters at position $i: $hexByte'); + } + result.add(byte); + } + return Uint8List.fromList(result); +} + // Command codes (to device) const int cmdAppStart = 1; const int cmdSendTxtMsg = 2; @@ -168,11 +170,13 @@ const int cmdGetChannel = 31; const int cmdSetChannel = 32; const int cmdSendTracePath = 36; const int cmdSetOtherParams = 38; -const int cmdGetRadioSettings = 57; +const int cmdSendAnonReq = 57; const int cmdGetTelemetryReq = 39; const int cmdGetCustomVar = 40; const int cmdSetCustomVar = 41; const int cmdSendBinaryReq = 50; +const int cmdSetAutoAddConfig = 58; +const int cmdGetAutoAddConfig = 59; // Text message types const int txtTypePlain = 0; @@ -206,8 +210,8 @@ const int respCodeDeviceInfo = 13; const int respCodeContactMsgRecvV3 = 16; const int respCodeChannelMsgRecvV3 = 17; const int respCodeChannelInfo = 18; -const int respCodeRadioSettings = 25; const int respCodeCustomVars = 21; +const int respCodeAutoAddConfig = 25; // Push codes (async from device) const int pushCodeAdvert = 0x80; @@ -253,6 +257,18 @@ const int payloadTypeCONTROL = 0x0B; // a control/discovery packet const int payloadTypeRawCustom = 0x0F; // custom packet as raw bytes, for applications with custom encryption, payloads, etc +//auto-add flags +const int autoAddOverwriteOldestFlag = + 1 << 0; // 0x01 - overwrite oldest non-favourite when full +const int autoAddChatFlag = + 1 << 1; // 0x02 - auto-add Chat (Companion) (ADV_TYPE_CHAT) +const int autoAddRepeaterFlag = + 1 << 2; // 0x04 - auto-add Repeater (ADV_TYPE_REPEATER) +const int autoAddRoomServerFlag = + 1 << 3; // 0x08 - auto-add Room Server (ADV_TYPE_ROOM) +const int autoAddSensorFlag = + 1 << 4; // 0x10 - auto-add Sensor (ADV_TYPE_SENSOR) + // Sizes const int pubKeySize = 32; const int maxPathSize = 64; @@ -303,7 +319,7 @@ const int contactNameOffset = 100; const int contactTimestampOffset = 132; const int contactLatOffset = 136; const int contactLonOffset = 140; -const int contactLastmodOffset = 144; +const int contactLastModOffset = 144; const int contactFrameSize = 148; // Message frame offsets @@ -681,16 +697,15 @@ Uint8List buildGetContactByKeyFrame(Uint8List pubKey) { return writer.toBytes(); } -// Build CMD_GET_RADIO_SETTINGS frame -Uint8List buildGetRadioSettingsFrame() { - return Uint8List.fromList([cmdGetRadioSettings]); -} - //Build CMD_GET_CUSTOM_VARS frame Uint8List buildGetCustomVarsFrame() { return Uint8List.fromList([cmdGetCustomVar]); } +Uint8List buildGetAutoAddFlagsFrame() { + return Uint8List.fromList([cmdGetAutoAddConfig]); +} + // Calculate LoRa airtime for a packet // Based on Semtech SX127x datasheet formula // Returns airtime in milliseconds @@ -815,10 +830,10 @@ Uint8List buildExportContactFrame(Uint8List pubKey) { // Build a import contact frame // [cmd][contact_frame x98+] -Uint8List buildImportContactFrame(String contactFrame) { +Uint8List buildImportContactFrame(Uint8List contactFrame) { final writer = BufferWriter(); writer.writeByte(cmdImportContact); - writer.writeHex(contactFrame); + writer.writeBytes(contactFrame); return writer.toBytes(); } @@ -832,20 +847,40 @@ Uint8List buildZeroHopContact(Uint8List pubKey) { } // Build CMD_SET_OTHER_PARAMS frame -// Format: [cmd][allowAutoAddContacts][allowTelemetryFlags][advertLocationPolicy][multiAcks] +// Format: [cmd][allowTelemetryFlags][advertLocationPolicy][multiAcks] Uint8List buildSetOtherParamsFrame( - bool allowAutoAddContacts, int allowTelemetryFlags, int advertLocationPolicy, int multiAcks, ) { final writer = BufferWriter(); writer.writeByte(cmdSetOtherParams); - writer.writeByte( - allowAutoAddContacts ? 0x00 : 0x01, - ); // Allow Auto Add Contacts + //Going forward the app will just set Auto Add Contacts to disabled, and use the filter flags + //Allow Auto Add Contacts use inverted logic (0x01 = disabled, 0x00 = enabled). + writer.writeByte(0x01); writer.writeByte(allowTelemetryFlags); // Allow Telemetry Flags writer.writeByte(advertLocationPolicy); // Advertisement Location Policy writer.writeByte(multiAcks); // Multi Acknowledgements return writer.toBytes(); } + +// Build CMD_SET_AUTO_ADD_CONFIG frame +// Format: [cmd][flags] +Uint8List buildSetAutoAddConfigFrame({ + required bool autoAddChat, + required bool autoAddRepeater, + required bool autoAddRoomServer, + required bool autoAddSensor, + required bool overwriteOldest, +}) { + final writer = BufferWriter(); + writer.writeByte(cmdSetAutoAddConfig); + int flags = 0; + if (autoAddChat) flags |= autoAddChatFlag; + if (autoAddRepeater) flags |= autoAddRepeaterFlag; + if (autoAddRoomServer) flags |= autoAddRoomServerFlag; + if (autoAddSensor) flags |= autoAddSensorFlag; + if (overwriteOldest) flags |= autoAddOverwriteOldestFlag; + writer.writeByte(flags); + return writer.toBytes(); +} diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 379dd47..9733738 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1802,23 +1802,31 @@ "contacts_searchRepeaters": "Търсене на {number}{str} повтарящи се...", "contacts_searchContactsNoNumber": "Търси контакти...", "contacts_searchUsers": "Търсене на {number}{str} потребители...", - "usbScreenSubtitle": "Изберете открит сериен уред и се свържете директно към вашия MeshCore възел.", - "usbScreenStatus": "Изберете USB устройство", - "usbScreenTitle": "Свързване чрез USB", - "usbScreenNote": "USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.", - "usbScreenEmptyState": "Няма открити USB устройства. Включете едно и опитайте отново.", - "usbErrorPermissionDenied": "Не беше разрешено достъпът през USB.", - "usbErrorDeviceMissing": "Избраното USB устройство вече не е налично.", - "usbErrorInvalidPort": "Изберете валидно USB устройство.", - "usbErrorBusy": "Друг мол за свързване през USB вече е в процес на изпълнение.", - "usbErrorNotConnected": "Няма свързано USB устройство.", - "usbErrorOpenFailed": "Не успях да отворя избраното USB устройство.", - "usbErrorConnectFailed": "Не успях да се свържа с избраното USB устройство.", - "usbErrorUnsupported": "USB серийната комуникация не се поддържа на тази платформа.", - "usbErrorAlreadyActive": "USB връзката вече е активирана.", - "usbErrorNoDeviceSelected": "Няма избран USB устройство.", - "usbErrorPortClosed": "USB връзката не е активна.", - "usbErrorConnectTimedOut": "Изчаква се, но устройството не отговаря в рамките на зададения време.", - "connectionChoiceBluetoothLabel": "Bluetooth", - "connectionChoiceUsbLabel": "USB" + "contactsSettings_title": "Настройки на контактите", + "contactsSettings_autoAddTitle": "Автоматично откриване", + "contactsSettings_autoAddUsersTitle": "Автоматично добавяне на потребители", + "contactsSettings_otherTitle": "Други настройки свързани с контакти", + "settings_contactSettingsSubtitle": "Настройки за добавяне на контакти.", + "settings_contactSettings": "Настройки за контакти", + "contactsSettings_autoAddSensorsTitle": "Автоматично добавяне на датчици", + "contactsSettings_autoAddRoomServersTitle": "Автоматично добавяне на сървъри на стаите", + "contactsSettings_autoAddRoomServersSubtitle": "Позволи на спътника да добавя автоматично откритите сървъри на стаите.", + "contactsSettings_autoAddRepeatersTitle": "Автоматично добавяне на повтарящи се елементи", + "contactsSettings_autoAddUsersSubtitle": "Позволи на спътника да добавя автоматично откритите потребители.", + "contactsSettings_autoAddRepeatersSubtitle": "Позволи на спътника да добавя автоматично откритите повтарящи се устройства.", + "contactsSettings_autoAddSensorsSubtitle": "Позволи на спътника да добавя автоматично откритите датчици.", + "contactsSettings_overwriteOldestTitle": "Премахни най-старото", + "discoveredContacts_Title": "Открити контакти", + "discoveredContacts_searchHint": "Търсене на открити контакти", + "discoveredContacts_noMatching": "Няма съвпадащи контакти", + "discoveredContacts_contactAdded": "Контакт добавен", + "discoveredContacts_copyContact": "Копирай контакт в клипборда", + "discoveredContacts_deleteContact": "Изтрий контакт", + "discoveredContacts_addContact": "Добави контакт", + "contactsSettings_overwriteOldestSubtitle": "Когато списъкът с контакти е пълен, най-старият неключов контакт ще бъде заменен.", + "discoveredContacts_deleteContactAll": "Изтриване на Всички Открити Контакти", + "discoveredContacts_deleteContactAllContent": "Сигурни ли сте, че искате да изтриете всички открити контакти?", + "common_deleteAll": "Изтрий всичко", + "map_guessedLocation": "Предполагано местоположение", + "map_showGuessedLocations": "Покажете местоположенията на предположените възли." } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 3588ab7..ecfd8e4 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1830,23 +1830,31 @@ "contacts_searchFavorites": "Suche {number}{str} Favoriten...", "contacts_searchUsers": "Suche {number}{str} Benutzer...", "contacts_searchRoomServers": "Suche {number}{str} Raumserver...", - "usbScreenSubtitle": "Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.", - "usbScreenTitle": "Über USB verbinden", - "usbScreenNote": "USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.", - "usbScreenStatus": "Wählen Sie ein USB-Gerät aus", - "usbScreenEmptyState": "Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie.", - "usbErrorPermissionDenied": "Die USB-Berechtigung wurde abgelehnt.", - "usbErrorDeviceMissing": "Das ausgewählte USB-Gerät ist nicht mehr verfügbar.", - "usbErrorInvalidPort": "Wählen Sie ein gültiges USB-Gerät aus.", - "usbErrorBusy": "Eine weitere Anfrage für eine USB-Verbindung ist bereits in Bearbeitung.", - "usbErrorNotConnected": "Es ist kein USB-Gerät angeschlossen.", - "usbErrorOpenFailed": "Fehlgeschlagen beim Öffnen des ausgewählten USB-Geräts.", - "usbErrorConnectFailed": "Keine Verbindung zum ausgewählten USB-Gerät hergestellt.", - "usbErrorUnsupported": "Die Unterstützung für USB-Seriellschnittstellen ist auf dieser Plattform nicht vorhanden.", - "usbErrorAlreadyActive": "Eine USB-Verbindung ist bereits hergestellt.", - "usbErrorNoDeviceSelected": "Kein USB-Gerät wurde ausgewählt.", - "usbErrorPortClosed": "Die USB-Verbindung ist nicht aktiv.", - "usbErrorConnectTimedOut": "Die Wartezeit ist abgelaufen, da keine Antwort vom Gerät empfangen wurde.", - "connectionChoiceBluetoothLabel": "Bluetooth", - "connectionChoiceUsbLabel": "USB" + "settings_contactSettings": "Kontakteinstellungen", + "contactsSettings_otherTitle": "Weitere Einstellungen zu Kontakten", + "contactsSettings_title": "Kontakteinstellungen", + "contactsSettings_autoAddTitle": "Automatische Erkennung", + "contactsSettings_autoAddUsersTitle": "Automatische Hinzufügung von Benutzern", + "settings_contactSettingsSubtitle": "Einstellungen für das Hinzufügen von Kontakten", + "contactsSettings_autoAddSensorsTitle": "Automatisch Sensoren hinzufügen", + "contactsSettings_autoAddUsersSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Benutzer hinzuzufügen", + "contactsSettings_autoAddRoomServersTitle": "Automatisch Raumservers hinzufügen", + "contactsSettings_autoAddRoomServersSubtitle": "Ermöglichen Sie dem Begleiter, entdeckte Raumserver automatisch hinzuzufügen", + "contactsSettings_autoAddRepeatersTitle": "Automatisch Repeater hinzufügen", + "contactsSettings_autoAddRepeatersSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Repeater hinzuzufügen.", + "discoveredContacts_noMatching": "Keine passenden Kontakte", + "discoveredContacts_searchHint": "Entdeckte Kontakte suchen", + "discoveredContacts_addContact": "Kontakt hinzufügen", + "discoveredContacts_contactAdded": "Kontakt hinzugefügt", + "discoveredContacts_deleteContact": "Kontakt löschen", + "discoveredContacts_Title": "Entdeckte Kontakte", + "discoveredContacts_copyContact": "Kontakt in die Zwischenablage kopieren", + "contactsSettings_overwriteOldestTitle": "Überschreiben des Ältesten", + "contactsSettings_autoAddSensorsSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Sensoren hinzuzufügen", + "contactsSettings_overwriteOldestSubtitle": "Wenn die Kontaktliste voll ist, wird der älteste nicht favorisierte Kontakt ersetzt.", + "common_deleteAll": "Alles löschen", + "discoveredContacts_deleteContactAllContent": "Sind Sie sicher, dass Sie alle gefundenen Kontakte löschen möchten?", + "discoveredContacts_deleteContactAll": "Alle entdeckten Kontakte löschen", + "map_showGuessedLocations": "Zeige die vermuteten Knotenpositionen", + "map_guessedLocation": "Geschätzter Ort" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 87aa1e1..4180629 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -10,6 +10,7 @@ "common_unknownDevice": "Unknown Device", "common_save": "Save", "common_delete": "Delete", + "common_deleteAll": "Delete All", "common_close": "Close", "common_edit": "Edit", "common_add": "Add", @@ -119,6 +120,8 @@ "settings_locationIntervalInvalid": "Interval must be at least 60 seconds, and less than 86400 seconds.", "settings_latitude": "Latitude", "settings_longitude": "Longitude", + "settings_contactSettings": "Contact Settings", + "settings_contactSettingsSubtitle": "Settings for how contacts are added.", "settings_privacyMode": "Privacy Mode", "settings_privacyModeSubtitle": "Hide name/location in advertisements", "settings_privacyModeToggle": "Toggle privacy mode to hide your name and location in advertisements.", @@ -791,6 +794,8 @@ "map_publicKeyPrefix": "Public key prefix", "map_markers": "Markers", "map_showSharedMarkers": "Show shared markers", + "map_showGuessedLocations": "Show guessed node locations", + "map_guessedLocation": "Guessed location", "map_lastSeenTime": "Last Seen Time", "map_sharedPin": "Shared pin", "map_joinRoom": "Join Room", @@ -1858,5 +1863,27 @@ "settings_gpxExportShareText": "Map data exported from meshcore-open", "settings_gpxExportShareSubject": "meshcore-open GPX map data export", "snrIndicator_nearByRepeaters": "Nearby Repeaters", - "snrIndicator_lastSeen": "Last seen" -} + "snrIndicator_lastSeen": "Last seen", + "contactsSettings_title": "Contacts settings", + "contactsSettings_autoAddTitle": "Automatic Discovery", + "contactsSettings_otherTitle": "Other contact related settings", + "contactsSettings_autoAddUsersTitle": "Auto-add users", + "contactsSettings_autoAddUsersSubtitle": "Allow the companion to automatically add discovered users.", + "contactsSettings_autoAddRepeatersTitle": "Auto-add repeaters", + "contactsSettings_autoAddRepeatersSubtitle": "Allow the companion to automatically add discovered repeaters.", + "contactsSettings_autoAddRoomServersTitle": "Auto-add room servers", + "contactsSettings_autoAddRoomServersSubtitle": "Allow the companion to automatically add discovered room servers.", + "contactsSettings_autoAddSensorsTitle": "Auto-add sensors", + "contactsSettings_autoAddSensorsSubtitle": "Allow the companion to automatically add discovered sensors.", + "contactsSettings_overwriteOldestTitle": "Overwrite Oldest", + "contactsSettings_overwriteOldestSubtitle": "When the contact list is full, the oldest non-favorited contact will be replaced.", + "discoveredContacts_Title": "Discovered Contacts", + "discoveredContacts_noMatching": "No matching contacts", + "discoveredContacts_searchHint": "Search discovered contacts", + "discoveredContacts_contactAdded": "Contact added", + "discoveredContacts_addContact": "Add Contact", + "discoveredContacts_copyContact": "Copy Contact to clipboard", + "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 b279dd6..82ffa55 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1830,23 +1830,31 @@ "contacts_searchUsers": "Buscar {number}{str} Usuarios...", "contacts_searchRepeaters": "Buscar {number}{str} Repetidores...", "contacts_searchRoomServers": "Buscar {number}{str} servidores de sala...", - "usbScreenTitle": "Conecte mediante USB", - "usbScreenNote": "La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.", - "usbScreenStatus": "Seleccione un dispositivo USB", - "usbScreenSubtitle": "Seleccione un dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.", - "usbScreenEmptyState": "No se encontraron dispositivos USB. Conecte uno y vuelva a cargar.", - "usbErrorPermissionDenied": "Se denegó el permiso de acceso a través de USB.", - "usbErrorDeviceMissing": "El dispositivo USB seleccionado ya no está disponible.", - "usbErrorInvalidPort": "Seleccione un dispositivo USB válido.", - "usbErrorBusy": "Ya se ha iniciado una solicitud de conexión USB adicional.", - "usbErrorNotConnected": "No hay ningún dispositivo USB conectado.", - "usbErrorOpenFailed": "No se pudo abrir el dispositivo USB seleccionado.", - "usbErrorConnectFailed": "No se pudo conectar con el dispositivo USB seleccionado.", - "usbErrorUnsupported": "La comunicación serial mediante USB no está soportada en esta plataforma.", - "usbErrorAlreadyActive": "La conexión USB ya está activa.", - "usbErrorNoDeviceSelected": "No se ha seleccionado ningún dispositivo USB.", - "usbErrorPortClosed": "La conexión USB no está activa.", - "usbErrorConnectTimedOut": "Se ha producido un error debido a la espera prolongada para recibir una respuesta del dispositivo.", - "connectionChoiceUsbLabel": "USB", - "connectionChoiceBluetoothLabel": "Bluetooth" + "contactsSettings_autoAddTitle": "Detección automática", + "settings_contactSettings": "Configuración de contacto", + "contactsSettings_autoAddUsersTitle": "Agregar usuarios automáticamente", + "contactsSettings_otherTitle": "Otras configuraciones relacionadas con el contacto", + "contactsSettings_autoAddUsersSubtitle": "Permitir que el compañero agregue automáticamente a los usuarios descubiertos.", + "contactsSettings_autoAddRepeatersSubtitle": "Permitir que el compañero agregue automáticamente los repetidores descubiertos.", + "contactsSettings_autoAddRoomServersSubtitle": "Permitir que el compañero agregue automáticamente los servidores de salas descubiertos.", + "contactsSettings_autoAddSensorsTitle": "Agregar sensores automáticamente", + "contactsSettings_title": "Configuración de contactos", + "settings_contactSettingsSubtitle": "Configuración de cómo se agregan los contactos.", + "contactsSettings_autoAddSensorsSubtitle": "Permitir que el compañero agregue automáticamente los sensores descubiertos.", + "contactsSettings_autoAddRepeatersTitle": "Agregar repetidores automáticamente", + "contactsSettings_overwriteOldestTitle": "Sobreescribir el más antiguo", + "contactsSettings_autoAddRoomServersTitle": "Agregar automáticamente servidores de sala", + "discoveredContacts_noMatching": "No se encontraron contactos coincidentes", + "discoveredContacts_contactAdded": "Contacto agregado", + "discoveredContacts_copyContact": "Copiar contacto al portapapeles", + "discoveredContacts_deleteContact": "Eliminar contacto", + "discoveredContacts_Title": "Contactos descubiertos", + "discoveredContacts_searchHint": "Buscar contactos descubiertos", + "discoveredContacts_addContact": "Agregar contacto", + "contactsSettings_overwriteOldestSubtitle": "Cuando la lista de contactos esté llena, se reemplazará el contacto no favorito más antiguo.", + "common_deleteAll": "Eliminar todo", + "discoveredContacts_deleteContactAll": "Eliminar Todos los Contactos Descubiertos", + "discoveredContacts_deleteContactAllContent": "¿Está seguro de que desea eliminar todos los contactos descubiertos!", + "map_guessedLocation": "Ubicación estimada", + "map_showGuessedLocations": "Mostrar las ubicaciones estimadas de los nodos." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 7265974..42d6ecb 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1802,23 +1802,31 @@ "contacts_searchRoomServers": "Rechercher {number}{str} serveurs de salle...", "contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...", "contacts_searchContactsNoNumber": "Rechercher des contacts...", - "usbScreenNote": "La communication série USB est active sur les appareils Android et les plateformes de bureau pris en charge.", - "usbScreenSubtitle": "Sélectionnez un périphérique série détecté et connectez-vous directement à votre nœud MeshCore.", - "usbScreenTitle": "Connectez via USB", - "usbScreenStatus": "Sélectionnez un périphérique USB", - "usbScreenEmptyState": "Aucun périphérique USB n'a été trouvé. Veuillez connecter un périphérique et rafraîchir la page.", - "usbErrorPermissionDenied": "L'accès via USB a été refusé.", - "usbErrorDeviceMissing": "Le périphérique USB sélectionné n'est plus disponible.", - "usbErrorInvalidPort": "Sélectionnez un périphérique USB valide.", - "usbErrorBusy": "Une autre demande de connexion USB est déjà en cours.", - "usbErrorNotConnected": "Aucun appareil USB n'est connecté.", - "usbErrorOpenFailed": "Impossible d'ouvrir l'appareil USB sélectionné.", - "usbErrorConnectFailed": "Impossible de se connecter à l'appareil USB sélectionné.", - "usbErrorUnsupported": "La communication série USB n'est pas prise en charge sur cette plateforme.", - "usbErrorAlreadyActive": "Une connexion USB est déjà établie.", - "usbErrorNoDeviceSelected": "Aucun appareil USB n'a été sélectionné.", - "usbErrorPortClosed": "La connexion USB n'est pas établie.", - "usbErrorConnectTimedOut": "Attente avec délai, en attendant une réponse de l'appareil.", - "connectionChoiceBluetoothLabel": "Bluetooth", - "connectionChoiceUsbLabel": "USB" + "settings_contactSettings": "Paramètres de contact", + "settings_contactSettingsSubtitle": "Paramètres pour l'ajout de contacts", + "contactsSettings_autoAddRepeatersTitle": "Ajouter automatiquement les répéteurs", + "contactsSettings_autoAddRepeatersSubtitle": "Autoriser le compagnon à ajouter automatiquement les répéteurs découverts", + "contactsSettings_autoAddRoomServersTitle": "Ajouter automatiquement les serveurs de salle", + "contactsSettings_autoAddRoomServersSubtitle": "Autoriser le compagnon à ajouter automatiquement les serveurs de salles découverts", + "contactsSettings_otherTitle": "Autres paramètres liés aux contacts", + "contactsSettings_title": "Paramètres des contacts", + "contactsSettings_autoAddUsersTitle": "Ajouter automatiquement les utilisateurs", + "contactsSettings_autoAddTitle": "Découverte automatique", + "contactsSettings_autoAddSensorsTitle": "Ajouter automatiquement les capteurs", + "contactsSettings_autoAddUsersSubtitle": "Autoriser le compagnon à ajouter automatiquement les utilisateurs découverts", + "discoveredContacts_noMatching": "Aucun contact correspondant", + "discoveredContacts_contactAdded": "Contact ajouté", + "discoveredContacts_addContact": "Ajouter un contact", + "discoveredContacts_copyContact": "Copier le contact dans le presse-papiers", + "discoveredContacts_deleteContact": "Supprimer le contact", + "contactsSettings_overwriteOldestTitle": "Écraser le plus ancien", + "contactsSettings_autoAddSensorsSubtitle": "Autoriser le compagnon à ajouter automatiquement les capteurs découverts.", + "discoveredContacts_Title": "Contacts découverts", + "discoveredContacts_searchHint": "Rechercher des contacts découverts", + "contactsSettings_overwriteOldestSubtitle": "Lorsque la liste de contacts est pleine, le contact le plus ancien non favori sera remplacé.", + "common_deleteAll": "Supprimer tout", + "discoveredContacts_deleteContactAll": "Supprimer tous les contacts découverts", + "discoveredContacts_deleteContactAllContent": "Êtes-vous sûr de vouloir supprimer tous les contacts découverts ?", + "map_showGuessedLocations": "Afficher les emplacements des nœuds estimés", + "map_guessedLocation": "Lieu deviné" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 7ce77ea..f3f3f53 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1802,23 +1802,31 @@ "contacts_unread": "Non letti", "contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...", "contacts_searchRoomServers": "Cerca {number}{str} server Room...", - "usbScreenNote": "La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.", - "usbScreenSubtitle": "Seleziona il dispositivo seriale rilevato e connettilo direttamente al tuo nodo MeshCore.", - "usbScreenStatus": "Seleziona un dispositivo USB", - "usbScreenTitle": "Connessione tramite USB", - "usbScreenEmptyState": "Nessun dispositivo USB rilevato. Collegare uno e riavviare.", - "usbErrorPermissionDenied": "È stato negato l'accesso tramite USB.", - "usbErrorDeviceMissing": "Il dispositivo USB selezionato non è più disponibile.", - "usbErrorInvalidPort": "Seleziona un dispositivo USB valido.", - "usbErrorBusy": "Un'altra richiesta di connessione tramite USB è già in corso.", - "usbErrorNotConnected": "Non è collegato alcun dispositivo USB.", - "usbErrorOpenFailed": "Impossibile aprire il dispositivo USB selezionato.", - "usbErrorConnectFailed": "Impossibile connettersi al dispositivo USB selezionato.", - "usbErrorUnsupported": "La comunicazione seriale tramite USB non è supportata su questa piattaforma.", - "usbErrorAlreadyActive": "La connessione USB è già attiva.", - "usbErrorNoDeviceSelected": "Non è stato selezionato alcun dispositivo USB.", - "usbErrorPortClosed": "La connessione USB non è attiva.", - "usbErrorConnectTimedOut": "Attesa superata, in attesa di una risposta dal dispositivo.", - "connectionChoiceUsbLabel": "USB", - "connectionChoiceBluetoothLabel": "Bluetooth" + "contactsSettings_title": "Impostazioni dei contatti", + "settings_contactSettings": "Impostazioni di contatto", + "contactsSettings_otherTitle": "Altre impostazioni relative ai contatti", + "contactsSettings_autoAddUsersSubtitle": "Consenti al compagno di aggiungere automaticamente gli utenti scoperti.", + "contactsSettings_autoAddRepeatersTitle": "Aggiungere ripetitori automaticamente", + "contactsSettings_autoAddRoomServersSubtitle": "Consenti al compagno di aggiungere automaticamente i server delle stanze scoperte.", + "contactsSettings_autoAddSensorsTitle": "Aggiungere automaticamente i sensori", + "settings_contactSettingsSubtitle": "Impostazioni per l'aggiunta dei contatti", + "contactsSettings_autoAddUsersTitle": "Aggiungere utenti automaticamente", + "contactsSettings_autoAddTitle": "Scoperta automatica", + "contactsSettings_autoAddSensorsSubtitle": "Consenti al compagno di aggiungere automaticamente i sensori scoperti", + "discoveredContacts_noMatching": "Nessun contatto corrispondente", + "contactsSettings_autoAddRepeatersSubtitle": "Consenti al compagno di aggiungere automaticamente i ripetitori scoperti.", + "discoveredContacts_searchHint": "Cerca contatti scoperti", + "contactsSettings_autoAddRoomServersTitle": "Aggiungere automaticamente i server delle stanze", + "discoveredContacts_addContact": "Aggiungi contatto", + "contactsSettings_overwriteOldestTitle": "Sostituisci il più vecchio", + "discoveredContacts_Title": "Contatti scoperti", + "discoveredContacts_contactAdded": "Contatto aggiunto", + "discoveredContacts_deleteContact": "Elimina Contatto", + "discoveredContacts_copyContact": "Copia contatto negli appunti", + "contactsSettings_overwriteOldestSubtitle": "Quando l'elenco dei contatti è pieno, il contatto più vecchio non tra i preferiti verrà sostituito.", + "common_deleteAll": "Elimina tutto", + "discoveredContacts_deleteContactAllContent": "Sei sicuro di voler eliminare tutti i contatti scoperti?", + "discoveredContacts_deleteContactAll": "Eliminare tutti i contatti scoperti", + "map_guessedLocation": "Località indovinata", + "map_showGuessedLocations": "Mostra le posizioni stimate dei nodi" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index a679063..ddf9de3 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -184,6 +184,12 @@ abstract class AppLocalizations { /// **'Delete'** String get common_delete; + /// No description provided for @common_deleteAll. + /// + /// In en, this message translates to: + /// **'Delete All'** + String get common_deleteAll; + /// No description provided for @common_close. /// /// In en, this message translates to: @@ -670,6 +676,18 @@ abstract class AppLocalizations { /// **'Longitude'** String get settings_longitude; + /// No description provided for @settings_contactSettings. + /// + /// In en, this message translates to: + /// **'Contact Settings'** + String get settings_contactSettings; + + /// No description provided for @settings_contactSettingsSubtitle. + /// + /// In en, this message translates to: + /// **'Settings for how contacts are added.'** + String get settings_contactSettingsSubtitle; + /// No description provided for @settings_privacyMode. /// /// In en, this message translates to: @@ -2734,6 +2752,18 @@ abstract class AppLocalizations { /// **'Show shared markers'** String get map_showSharedMarkers; + /// No description provided for @map_showGuessedLocations. + /// + /// In en, this message translates to: + /// **'Show guessed node locations'** + String get map_showGuessedLocations; + + /// No description provided for @map_guessedLocation. + /// + /// In en, this message translates to: + /// **'Guessed location'** + String get map_guessedLocation; + /// No description provided for @map_lastSeenTime. /// /// In en, this message translates to: @@ -5506,6 +5536,138 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Last seen'** String get snrIndicator_lastSeen; + + /// No description provided for @contactsSettings_title. + /// + /// In en, this message translates to: + /// **'Contacts settings'** + String get contactsSettings_title; + + /// No description provided for @contactsSettings_autoAddTitle. + /// + /// In en, this message translates to: + /// **'Automatic Discovery'** + String get contactsSettings_autoAddTitle; + + /// No description provided for @contactsSettings_otherTitle. + /// + /// In en, this message translates to: + /// **'Other contact related settings'** + String get contactsSettings_otherTitle; + + /// No description provided for @contactsSettings_autoAddUsersTitle. + /// + /// In en, this message translates to: + /// **'Auto-add users'** + String get contactsSettings_autoAddUsersTitle; + + /// No description provided for @contactsSettings_autoAddUsersSubtitle. + /// + /// In en, this message translates to: + /// **'Allow the companion to automatically add discovered users.'** + String get contactsSettings_autoAddUsersSubtitle; + + /// No description provided for @contactsSettings_autoAddRepeatersTitle. + /// + /// In en, this message translates to: + /// **'Auto-add repeaters'** + String get contactsSettings_autoAddRepeatersTitle; + + /// No description provided for @contactsSettings_autoAddRepeatersSubtitle. + /// + /// In en, this message translates to: + /// **'Allow the companion to automatically add discovered repeaters.'** + String get contactsSettings_autoAddRepeatersSubtitle; + + /// No description provided for @contactsSettings_autoAddRoomServersTitle. + /// + /// In en, this message translates to: + /// **'Auto-add room servers'** + String get contactsSettings_autoAddRoomServersTitle; + + /// No description provided for @contactsSettings_autoAddRoomServersSubtitle. + /// + /// In en, this message translates to: + /// **'Allow the companion to automatically add discovered room servers.'** + String get contactsSettings_autoAddRoomServersSubtitle; + + /// No description provided for @contactsSettings_autoAddSensorsTitle. + /// + /// In en, this message translates to: + /// **'Auto-add sensors'** + String get contactsSettings_autoAddSensorsTitle; + + /// No description provided for @contactsSettings_autoAddSensorsSubtitle. + /// + /// In en, this message translates to: + /// **'Allow the companion to automatically add discovered sensors.'** + String get contactsSettings_autoAddSensorsSubtitle; + + /// No description provided for @contactsSettings_overwriteOldestTitle. + /// + /// In en, this message translates to: + /// **'Overwrite Oldest'** + String get contactsSettings_overwriteOldestTitle; + + /// No description provided for @contactsSettings_overwriteOldestSubtitle. + /// + /// In en, this message translates to: + /// **'When the contact list is full, the oldest non-favorited contact will be replaced.'** + String get contactsSettings_overwriteOldestSubtitle; + + /// No description provided for @discoveredContacts_Title. + /// + /// In en, this message translates to: + /// **'Discovered Contacts'** + String get discoveredContacts_Title; + + /// No description provided for @discoveredContacts_noMatching. + /// + /// In en, this message translates to: + /// **'No matching contacts'** + String get discoveredContacts_noMatching; + + /// No description provided for @discoveredContacts_searchHint. + /// + /// In en, this message translates to: + /// **'Search discovered contacts'** + String get discoveredContacts_searchHint; + + /// No description provided for @discoveredContacts_contactAdded. + /// + /// In en, this message translates to: + /// **'Contact added'** + String get discoveredContacts_contactAdded; + + /// No description provided for @discoveredContacts_addContact. + /// + /// In en, this message translates to: + /// **'Add Contact'** + String get discoveredContacts_addContact; + + /// No description provided for @discoveredContacts_copyContact. + /// + /// In en, this message translates to: + /// **'Copy Contact to clipboard'** + String get discoveredContacts_copyContact; + + /// No description provided for @discoveredContacts_deleteContact. + /// + /// In en, this message translates to: + /// **'Delete Discovered Contact'** + String get discoveredContacts_deleteContact; + + /// No description provided for @discoveredContacts_deleteContactAll. + /// + /// In en, this message translates to: + /// **'Delete All Discovered Contacts'** + String get discoveredContacts_deleteContactAll; + + /// No description provided for @discoveredContacts_deleteContactAllContent. + /// + /// In en, this message translates to: + /// **'Are you sure you want to delete all discovered contacts?'** + String get discoveredContacts_deleteContactAllContent; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index b964966..5720b77 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -38,6 +38,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get common_delete => 'Изтрий'; + @override + String get common_deleteAll => 'Изтрий всичко'; + @override String get common_close => 'Затвори'; @@ -307,6 +310,13 @@ class AppLocalizationsBg extends AppLocalizations { @override String get settings_longitude => 'Дължина'; + @override + String get settings_contactSettings => 'Настройки за контакти'; + + @override + String get settings_contactSettingsSubtitle => + 'Настройки за добавяне на контакти.'; + @override String get settings_privacyMode => 'Режим на поверителност'; @@ -1499,6 +1509,13 @@ class AppLocalizationsBg extends AppLocalizations { @override String get map_showSharedMarkers => 'Покажи споделени маркери'; + @override + String get map_showGuessedLocations => + 'Покажете местоположенията на предположените възли.'; + + @override + String get map_guessedLocation => 'Предполагано местоположение'; + @override String get map_lastSeenTime => 'Последна видяна дата'; @@ -3185,4 +3202,82 @@ class AppLocalizationsBg extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Последно видян'; + + @override + String get contactsSettings_title => 'Настройки на контактите'; + + @override + String get contactsSettings_autoAddTitle => 'Автоматично откриване'; + + @override + String get contactsSettings_otherTitle => + 'Други настройки свързани с контакти'; + + @override + String get contactsSettings_autoAddUsersTitle => + 'Автоматично добавяне на потребители'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Позволи на спътника да добавя автоматично откритите потребители.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => + 'Автоматично добавяне на повтарящи се елементи'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Позволи на спътника да добавя автоматично откритите повтарящи се устройства.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Автоматично добавяне на сървъри на стаите'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Позволи на спътника да добавя автоматично откритите сървъри на стаите.'; + + @override + String get contactsSettings_autoAddSensorsTitle => + 'Автоматично добавяне на датчици'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Позволи на спътника да добавя автоматично откритите датчици.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Премахни най-старото'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'Когато списъкът с контакти е пълен, най-старият неключов контакт ще бъде заменен.'; + + @override + String get discoveredContacts_Title => 'Открити контакти'; + + @override + String get discoveredContacts_noMatching => 'Няма съвпадащи контакти'; + + @override + String get discoveredContacts_searchHint => 'Търсене на открити контакти'; + + @override + String get discoveredContacts_contactAdded => 'Контакт добавен'; + + @override + String get discoveredContacts_addContact => 'Добави контакт'; + + @override + String get discoveredContacts_copyContact => 'Копирай контакт в клипборда'; + + @override + String get discoveredContacts_deleteContact => 'Изтрий контакт'; + + @override + String get discoveredContacts_deleteContactAll => + 'Изтриване на Всички Открити Контакти'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Сигурни ли сте, че искате да изтриете всички открити контакти?'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index ed0ecf0..a3e985a 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -38,6 +38,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get common_delete => 'Löschen'; + @override + String get common_deleteAll => 'Alles löschen'; + @override String get common_close => 'Schließen'; @@ -308,6 +311,13 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_longitude => 'Längengrad'; + @override + String get settings_contactSettings => 'Kontakteinstellungen'; + + @override + String get settings_contactSettingsSubtitle => + 'Einstellungen für das Hinzufügen von Kontakten'; + @override String get settings_privacyMode => 'Privatsphäreeinstellung'; @@ -1500,6 +1510,13 @@ class AppLocalizationsDe extends AppLocalizations { @override String get map_showSharedMarkers => 'Zeige gemeinsam genutzte Marker'; + @override + String get map_showGuessedLocations => + 'Zeige die vermuteten Knotenpositionen'; + + @override + String get map_guessedLocation => 'Geschätzter Ort'; + @override String get map_lastSeenTime => 'Letzte Sichtung'; @@ -3196,4 +3213,84 @@ class AppLocalizationsDe extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Zuletzt gesehen'; + + @override + String get contactsSettings_title => 'Kontakteinstellungen'; + + @override + String get contactsSettings_autoAddTitle => 'Automatische Erkennung'; + + @override + String get contactsSettings_otherTitle => + 'Weitere Einstellungen zu Kontakten'; + + @override + String get contactsSettings_autoAddUsersTitle => + 'Automatische Hinzufügung von Benutzern'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Ermöglichen Sie dem Begleiter, automatisch entdeckte Benutzer hinzuzufügen'; + + @override + String get contactsSettings_autoAddRepeatersTitle => + 'Automatisch Repeater hinzufügen'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Ermöglichen Sie dem Begleiter, automatisch entdeckte Repeater hinzuzufügen.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Automatisch Raumservers hinzufügen'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Ermöglichen Sie dem Begleiter, entdeckte Raumserver automatisch hinzuzufügen'; + + @override + String get contactsSettings_autoAddSensorsTitle => + 'Automatisch Sensoren hinzufügen'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Ermöglichen Sie dem Begleiter, automatisch entdeckte Sensoren hinzuzufügen'; + + @override + String get contactsSettings_overwriteOldestTitle => + 'Überschreiben des Ältesten'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'Wenn die Kontaktliste voll ist, wird der älteste nicht favorisierte Kontakt ersetzt.'; + + @override + String get discoveredContacts_Title => 'Entdeckte Kontakte'; + + @override + String get discoveredContacts_noMatching => 'Keine passenden Kontakte'; + + @override + String get discoveredContacts_searchHint => 'Entdeckte Kontakte suchen'; + + @override + String get discoveredContacts_contactAdded => 'Kontakt hinzugefügt'; + + @override + String get discoveredContacts_addContact => 'Kontakt hinzufügen'; + + @override + String get discoveredContacts_copyContact => + 'Kontakt in die Zwischenablage kopieren'; + + @override + String get discoveredContacts_deleteContact => 'Kontakt löschen'; + + @override + String get discoveredContacts_deleteContactAll => + 'Alle entdeckten Kontakte löschen'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Sind Sie sicher, dass Sie alle gefundenen Kontakte löschen möchten?'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 05ed0b9..8655f42 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -38,6 +38,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get common_delete => 'Delete'; + @override + String get common_deleteAll => 'Delete All'; + @override String get common_close => 'Close'; @@ -304,6 +307,13 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settings_longitude => 'Longitude'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Privacy Mode'; @@ -1476,6 +1486,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get map_showSharedMarkers => 'Show shared markers'; + @override + String get map_showGuessedLocations => 'Show guessed node locations'; + + @override + String get map_guessedLocation => 'Guessed location'; + @override String get map_lastSeenTime => 'Last Seen Time'; @@ -3137,4 +3153,78 @@ class AppLocalizationsEn extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Last seen'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When the contact list is full, the oldest non-favorited contact will be replaced.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; + + @override + String get discoveredContacts_contactAdded => 'Contact added'; + + @override + String get discoveredContacts_addContact => 'Add Contact'; + + @override + String get discoveredContacts_copyContact => 'Copy Contact to clipboard'; + + @override + String get discoveredContacts_deleteContact => 'Delete Discovered Contact'; + + @override + String get discoveredContacts_deleteContactAll => + 'Delete All Discovered Contacts'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Are you sure you want to delete all discovered contacts?'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 98a3a1e..8c3d5a7 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -38,6 +38,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get common_delete => 'Eliminar'; + @override + String get common_deleteAll => 'Eliminar todo'; + @override String get common_close => 'Cerrar'; @@ -308,6 +311,13 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_longitude => 'Longitud'; + @override + String get settings_contactSettings => 'Configuración de contacto'; + + @override + String get settings_contactSettingsSubtitle => + 'Configuración de cómo se agregan los contactos.'; + @override String get settings_privacyMode => 'Modo Privacidad'; @@ -1498,6 +1508,13 @@ class AppLocalizationsEs extends AppLocalizations { @override String get map_showSharedMarkers => 'Mostrar marcadores compartidos'; + @override + String get map_showGuessedLocations => + 'Mostrar las ubicaciones estimadas de los nodos.'; + + @override + String get map_guessedLocation => 'Ubicación estimada'; + @override String get map_lastSeenTime => 'Última vez que se vio'; @@ -3188,4 +3205,85 @@ class AppLocalizationsEs extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Visto por última vez'; + + @override + String get contactsSettings_title => 'Configuración de contactos'; + + @override + String get contactsSettings_autoAddTitle => 'Detección automática'; + + @override + String get contactsSettings_otherTitle => + 'Otras configuraciones relacionadas con el contacto'; + + @override + String get contactsSettings_autoAddUsersTitle => + 'Agregar usuarios automáticamente'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Permitir que el compañero agregue automáticamente a los usuarios descubiertos.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => + 'Agregar repetidores automáticamente'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Permitir que el compañero agregue automáticamente los repetidores descubiertos.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Agregar automáticamente servidores de sala'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Permitir que el compañero agregue automáticamente los servidores de salas descubiertos.'; + + @override + String get contactsSettings_autoAddSensorsTitle => + 'Agregar sensores automáticamente'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Permitir que el compañero agregue automáticamente los sensores descubiertos.'; + + @override + String get contactsSettings_overwriteOldestTitle => + 'Sobreescribir el más antiguo'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'Cuando la lista de contactos esté llena, se reemplazará el contacto no favorito más antiguo.'; + + @override + String get discoveredContacts_Title => 'Contactos descubiertos'; + + @override + String get discoveredContacts_noMatching => + 'No se encontraron contactos coincidentes'; + + @override + String get discoveredContacts_searchHint => 'Buscar contactos descubiertos'; + + @override + String get discoveredContacts_contactAdded => 'Contacto agregado'; + + @override + String get discoveredContacts_addContact => 'Agregar contacto'; + + @override + String get discoveredContacts_copyContact => + 'Copiar contacto al portapapeles'; + + @override + String get discoveredContacts_deleteContact => 'Eliminar contacto'; + + @override + String get discoveredContacts_deleteContactAll => + 'Eliminar Todos los Contactos Descubiertos'; + + @override + String get discoveredContacts_deleteContactAllContent => + '¿Está seguro de que desea eliminar todos los contactos descubiertos!'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 7e17de9..3b33741 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -38,6 +38,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get common_delete => 'Supprimer'; + @override + String get common_deleteAll => 'Supprimer tout'; + @override String get common_close => 'Fermer'; @@ -308,6 +311,13 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_longitude => 'Longitude'; + @override + String get settings_contactSettings => 'Paramètres de contact'; + + @override + String get settings_contactSettingsSubtitle => + 'Paramètres pour l\'ajout de contacts'; + @override String get settings_privacyMode => 'Mode de confidentialité'; @@ -1504,6 +1514,13 @@ class AppLocalizationsFr extends AppLocalizations { @override String get map_showSharedMarkers => 'Afficher les marqueurs partagés'; + @override + String get map_showGuessedLocations => + 'Afficher les emplacements des nœuds estimés'; + + @override + String get map_guessedLocation => 'Lieu deviné'; + @override String get map_lastSeenTime => 'Dernière fois vu'; @@ -3209,4 +3226,84 @@ class AppLocalizationsFr extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Dernière fois vu'; + + @override + String get contactsSettings_title => 'Paramètres des contacts'; + + @override + String get contactsSettings_autoAddTitle => 'Découverte automatique'; + + @override + String get contactsSettings_otherTitle => + 'Autres paramètres liés aux contacts'; + + @override + String get contactsSettings_autoAddUsersTitle => + 'Ajouter automatiquement les utilisateurs'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Autoriser le compagnon à ajouter automatiquement les utilisateurs découverts'; + + @override + String get contactsSettings_autoAddRepeatersTitle => + 'Ajouter automatiquement les répéteurs'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Autoriser le compagnon à ajouter automatiquement les répéteurs découverts'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Ajouter automatiquement les serveurs de salle'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Autoriser le compagnon à ajouter automatiquement les serveurs de salles découverts'; + + @override + String get contactsSettings_autoAddSensorsTitle => + 'Ajouter automatiquement les capteurs'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Autoriser le compagnon à ajouter automatiquement les capteurs découverts.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Écraser le plus ancien'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'Lorsque la liste de contacts est pleine, le contact le plus ancien non favori sera remplacé.'; + + @override + String get discoveredContacts_Title => 'Contacts découverts'; + + @override + String get discoveredContacts_noMatching => 'Aucun contact correspondant'; + + @override + String get discoveredContacts_searchHint => + 'Rechercher des contacts découverts'; + + @override + String get discoveredContacts_contactAdded => 'Contact ajouté'; + + @override + String get discoveredContacts_addContact => 'Ajouter un contact'; + + @override + String get discoveredContacts_copyContact => + 'Copier le contact dans le presse-papiers'; + + @override + String get discoveredContacts_deleteContact => 'Supprimer le contact'; + + @override + String get discoveredContacts_deleteContactAll => + 'Supprimer tous les contacts découverts'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Êtes-vous sûr de vouloir supprimer tous les contacts découverts ?'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index d2a832e..1cea4f6 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -38,6 +38,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get common_delete => 'Elimina'; + @override + String get common_deleteAll => 'Elimina tutto'; + @override String get common_close => 'Chiudi'; @@ -308,6 +311,13 @@ class AppLocalizationsIt extends AppLocalizations { @override String get settings_longitude => 'Longitudine'; + @override + String get settings_contactSettings => 'Impostazioni di contatto'; + + @override + String get settings_contactSettingsSubtitle => + 'Impostazioni per l\'aggiunta dei contatti'; + @override String get settings_privacyMode => 'Modalità Privacy'; @@ -1497,6 +1507,12 @@ class AppLocalizationsIt extends AppLocalizations { @override String get map_showSharedMarkers => 'Mostra i segnaposto condivisi'; + @override + String get map_showGuessedLocations => 'Mostra le posizioni stimate dei nodi'; + + @override + String get map_guessedLocation => 'Località indovinata'; + @override String get map_lastSeenTime => 'Ultimo Tempo di Visualizzazione'; @@ -3191,4 +3207,83 @@ class AppLocalizationsIt extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Ultimo accesso'; + + @override + String get contactsSettings_title => 'Impostazioni dei contatti'; + + @override + String get contactsSettings_autoAddTitle => 'Scoperta automatica'; + + @override + String get contactsSettings_otherTitle => + 'Altre impostazioni relative ai contatti'; + + @override + String get contactsSettings_autoAddUsersTitle => + 'Aggiungere utenti automaticamente'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Consenti al compagno di aggiungere automaticamente gli utenti scoperti.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => + 'Aggiungere ripetitori automaticamente'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Consenti al compagno di aggiungere automaticamente i ripetitori scoperti.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Aggiungere automaticamente i server delle stanze'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Consenti al compagno di aggiungere automaticamente i server delle stanze scoperte.'; + + @override + String get contactsSettings_autoAddSensorsTitle => + 'Aggiungere automaticamente i sensori'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Consenti al compagno di aggiungere automaticamente i sensori scoperti'; + + @override + String get contactsSettings_overwriteOldestTitle => + 'Sostituisci il più vecchio'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'Quando l\'elenco dei contatti è pieno, il contatto più vecchio non tra i preferiti verrà sostituito.'; + + @override + String get discoveredContacts_Title => 'Contatti scoperti'; + + @override + String get discoveredContacts_noMatching => 'Nessun contatto corrispondente'; + + @override + String get discoveredContacts_searchHint => 'Cerca contatti scoperti'; + + @override + String get discoveredContacts_contactAdded => 'Contatto aggiunto'; + + @override + String get discoveredContacts_addContact => 'Aggiungi contatto'; + + @override + String get discoveredContacts_copyContact => 'Copia contatto negli appunti'; + + @override + String get discoveredContacts_deleteContact => 'Elimina Contatto'; + + @override + String get discoveredContacts_deleteContactAll => + 'Eliminare tutti i contatti scoperti'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Sei sicuro di voler eliminare tutti i contatti scoperti?'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index c83cab8..dbd3290 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -38,6 +38,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get common_delete => 'Verwijderen'; + @override + String get common_deleteAll => 'Alles verwijderen'; + @override String get common_close => 'Sluiten'; @@ -306,6 +309,13 @@ class AppLocalizationsNl extends AppLocalizations { @override String get settings_longitude => 'Lengtegraad'; + @override + String get settings_contactSettings => 'Contactinstellingen'; + + @override + String get settings_contactSettingsSubtitle => + 'Instellingen voor het toevoegen van contacten'; + @override String get settings_privacyMode => 'Privacy Mode'; @@ -1490,6 +1500,13 @@ class AppLocalizationsNl extends AppLocalizations { @override String get map_showSharedMarkers => 'Toon gedeelde markeringen'; + @override + String get map_showGuessedLocations => + 'Toon de voorspelde locaties van de knopen'; + + @override + String get map_guessedLocation => 'Geroerde locatie'; + @override String get map_lastSeenTime => 'Laatste Bekeken Tijd'; @@ -3176,4 +3193,82 @@ class AppLocalizationsNl extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Laatst gezien'; + + @override + String get contactsSettings_title => 'Instellingen voor contacten'; + + @override + String get contactsSettings_autoAddTitle => 'Automatische detectie'; + + @override + String get contactsSettings_otherTitle => + 'Andere instellingen voor contactgerelateerde zaken'; + + @override + String get contactsSettings_autoAddUsersTitle => + 'Gebruikers automatisch toevoegen'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Sta toe dat de companion automatisch ontdekte gebruikers toevoegt'; + + @override + String get contactsSettings_autoAddRepeatersTitle => + 'Automatisch herhalingstoestellen toevoegen'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Sta toe dat de companion automatisch ontdekte repeaters toevoegt'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Automatisch kamerservers toevoegen'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Sta toe dat de companion automatisch ontdekte kamer servers toevoegt.'; + + @override + String get contactsSettings_autoAddSensorsTitle => + 'Automatisch sensoren toevoegen'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Sta toe dat de companion automatisch ontdekte sensoren toevoegt'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overschrijf Oudste'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'Wanneer de contactenlijst vol is, wordt de oudste niet-favoriete contactpersoon vervangen.'; + + @override + String get discoveredContacts_Title => 'Ontdekte contacten'; + + @override + String get discoveredContacts_noMatching => 'Geen overeenkomende contacten'; + + @override + String get discoveredContacts_searchHint => 'Ontdekte contacten zoeken'; + + @override + String get discoveredContacts_contactAdded => 'Contact toegevoegd'; + + @override + String get discoveredContacts_addContact => 'Contact toevoegen'; + + @override + String get discoveredContacts_copyContact => 'Kopieer contact naar klembord'; + + @override + String get discoveredContacts_deleteContact => 'Contact verwijderen'; + + @override + String get discoveredContacts_deleteContactAll => + 'Verwijder alle ontdekte contacten'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Weet u zeker dat u alle ontdekte contacten wilt verwijderen?'; } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index fe9dedb..930a6a1 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -38,6 +38,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get common_delete => 'Usuń'; + @override + String get common_deleteAll => 'Usuń wszystko'; + @override String get common_close => 'Zamknąć'; @@ -310,6 +313,13 @@ class AppLocalizationsPl extends AppLocalizations { @override String get settings_longitude => 'Długość'; + @override + String get settings_contactSettings => 'Ustawienia kontaktowe'; + + @override + String get settings_contactSettingsSubtitle => + 'Ustawienia dotyczące sposobu dodawania kontaktów'; + @override String get settings_privacyMode => 'Tryb Prywatny'; @@ -1498,6 +1508,13 @@ class AppLocalizationsPl extends AppLocalizations { @override String get map_showSharedMarkers => 'Pokaż współdzielone znaki.'; + @override + String get map_showGuessedLocations => + 'Wyświetl lokalizacje zgadanych węzłów'; + + @override + String get map_guessedLocation => 'Wydana lokalizacja'; + @override String get map_lastSeenTime => 'Ostatni raz widiany'; @@ -3191,4 +3208,82 @@ class AppLocalizationsPl extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Ostatnio widziany'; + + @override + String get contactsSettings_title => 'Ustawienia kontaktów'; + + @override + String get contactsSettings_autoAddTitle => 'Automatyczne odnajdywanie'; + + @override + String get contactsSettings_otherTitle => + 'Inne ustawienia związane z kontaktami'; + + @override + String get contactsSettings_autoAddUsersTitle => + 'Automatycznie dodaj użytkowników'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Pozwól towarzyszowi automatycznie dodawać znalezione użytkowników.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => + 'Automatyczne dodawanie powtarzalników'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Zezwól na automatyczne dodawanie odkrytych repeaterów przez towarzysza.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Automatycznie dodaj serwery pokojowe'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Zezwól towarzyszowi na automatyczne dodawanie znalezionych serwerów pokojowych.'; + + @override + String get contactsSettings_autoAddSensorsTitle => + 'Automatycznie dodaj czujniki'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Zezwól towarzyszowi na automatyczne dodawanie wykrytych czujników.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Nadpisz najstarszy'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'Gdy lista kontaktów jest pełna, najstarszy nieulubiony kontakt zostanie zastąpiony.'; + + @override + String get discoveredContacts_Title => 'Odkryte Kontakty'; + + @override + String get discoveredContacts_noMatching => 'Brak pasujących kontaktów'; + + @override + String get discoveredContacts_searchHint => 'Wyszukaj odkryte kontakty'; + + @override + String get discoveredContacts_contactAdded => 'Kontakt dodany'; + + @override + String get discoveredContacts_addContact => 'Dodaj kontakt'; + + @override + String get discoveredContacts_copyContact => 'Kopiuj kontakt do schowka'; + + @override + String get discoveredContacts_deleteContact => 'Usuń kontakt'; + + @override + String get discoveredContacts_deleteContactAll => + 'Usuń wszystkie odkryte kontakty'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Czy na pewno chcesz usunąć wszystkie znalezione kontakty?'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index a767807..50c0a35 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -38,6 +38,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get common_delete => 'Excluir'; + @override + String get common_deleteAll => 'Excluir Tudo'; + @override String get common_close => 'Fechar'; @@ -309,6 +312,13 @@ class AppLocalizationsPt extends AppLocalizations { @override String get settings_longitude => 'Longitude'; + @override + String get settings_contactSettings => 'Configurações de Contato'; + + @override + String get settings_contactSettingsSubtitle => + 'Configurações para como os contatos são adicionados'; + @override String get settings_privacyMode => 'Modo de Privacidade'; @@ -1499,6 +1509,13 @@ class AppLocalizationsPt extends AppLocalizations { @override String get map_showSharedMarkers => 'Mostrar marcadores compartilhados'; + @override + String get map_showGuessedLocations => + 'Mostrar as localizações dos nós estimados'; + + @override + String get map_guessedLocation => 'Localização estimada'; + @override String get map_lastSeenTime => 'Último Tempo de Visualização'; @@ -3186,4 +3203,84 @@ class AppLocalizationsPt extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Visto pela última vez'; + + @override + String get contactsSettings_title => 'Configurações de contatos'; + + @override + String get contactsSettings_autoAddTitle => 'Descoberta Automática'; + + @override + String get contactsSettings_otherTitle => + 'Outras configurações relacionadas a contatos'; + + @override + String get contactsSettings_autoAddUsersTitle => + 'Adicionar usuários automaticamente'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Permitir que o companheiro adicione automaticamente os usuários descobertos.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => + 'Adicionar repetidores automaticamente'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Permitir que o companheiro adicione automaticamente os repetidores descobertos.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Adicionar automaticamente servidores de sala'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Permitir que o companheiro adicione automaticamente os servidores de salas descobertos.'; + + @override + String get contactsSettings_autoAddSensorsTitle => + 'Adicionar sensores automaticamente'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Permitir que o companheiro adicione automaticamente sensores descobertos.'; + + @override + String get contactsSettings_overwriteOldestTitle => + 'Sobrescrever o Mais Antigo'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'Quando a lista de contatos estiver cheia, o contato mais antigo não favoritado será substituído.'; + + @override + String get discoveredContacts_Title => 'Contatos Descobertos'; + + @override + String get discoveredContacts_noMatching => 'Nenhum contato correspondente'; + + @override + String get discoveredContacts_searchHint => 'Pesquisar contatos descobertos'; + + @override + String get discoveredContacts_contactAdded => 'Contato adicionado'; + + @override + String get discoveredContacts_addContact => 'Adicionar Contato'; + + @override + String get discoveredContacts_copyContact => + 'Copiar Contato para a área de transferência'; + + @override + String get discoveredContacts_deleteContact => 'Excluir Contato'; + + @override + String get discoveredContacts_deleteContactAll => + 'Excluir Todos os Contatos Descobertos'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Tem certeza de que deseja excluir todos os contatos descobertos?'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index dd95949..cec7a97 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -38,6 +38,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get common_delete => 'Удалить'; + @override + String get common_deleteAll => 'Удалить все'; + @override String get common_close => 'Закрыть'; @@ -308,6 +311,13 @@ class AppLocalizationsRu extends AppLocalizations { @override String get settings_longitude => 'Долгота'; + @override + String get settings_contactSettings => 'Настройки контактов'; + + @override + String get settings_contactSettingsSubtitle => + 'Настройки добавления контактов'; + @override String get settings_privacyMode => 'Режим конфиденциальности'; @@ -1501,6 +1511,13 @@ class AppLocalizationsRu extends AppLocalizations { @override String get map_showSharedMarkers => 'Показывать общие метки'; + @override + String get map_showGuessedLocations => + 'Отобразить предполагаемые места расположения узлов'; + + @override + String get map_guessedLocation => 'Угаданное место'; + @override String get map_lastSeenTime => 'Время последнего появления'; @@ -3199,4 +3216,84 @@ class AppLocalizationsRu extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Последний раз видели'; + + @override + String get contactsSettings_title => 'Настройки контактов'; + + @override + String get contactsSettings_autoAddTitle => 'Автоматическое обнаружение'; + + @override + String get contactsSettings_otherTitle => + 'Другие настройки, связанные с контактами'; + + @override + String get contactsSettings_autoAddUsersTitle => + 'Автоматически добавлять пользователей'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Разрешить компаньону автоматически добавлять обнаруженных пользователей'; + + @override + String get contactsSettings_autoAddRepeatersTitle => + 'Автоматически добавлять ретрансляторы'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Разрешить спутнику автоматически добавлять обнаруженные ретрансляторы'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Автоматически добавлять серверы комнат'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Разрешить компаньону автоматически добавлять обнаруженные сервера комнат.'; + + @override + String get contactsSettings_autoAddSensorsTitle => + 'Автоматически добавлять датчики'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Разрешить компаньону автоматически добавлять обнаруженные датчики'; + + @override + String get contactsSettings_overwriteOldestTitle => + 'Перезаписать самое старое'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'Когда список контактов заполнен, будет заменен самый старый контакт, который не находится в избранном.'; + + @override + String get discoveredContacts_Title => 'Обнаруженные контакты'; + + @override + String get discoveredContacts_noMatching => 'Нет совпадающих контактов'; + + @override + String get discoveredContacts_searchHint => 'Найденные контакты поиска'; + + @override + String get discoveredContacts_contactAdded => 'Контакт добавлен'; + + @override + String get discoveredContacts_addContact => 'Добавить контакт'; + + @override + String get discoveredContacts_copyContact => + 'Копировать контакт в буфер обмена'; + + @override + String get discoveredContacts_deleteContact => 'Удалить контакт'; + + @override + String get discoveredContacts_deleteContactAll => + 'Удалить Все Обнаруженные Контакты'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Вы уверены, что хотите удалить все обнаруженные контакты?'; } diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 65e6d36..a8db3ad 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -38,6 +38,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get common_delete => 'Odstrániť'; + @override + String get common_deleteAll => 'Zmazať všetko'; + @override String get common_close => 'Zavrieť'; @@ -308,6 +311,13 @@ class AppLocalizationsSk extends AppLocalizations { @override String get settings_longitude => 'Dĺžka'; + @override + String get settings_contactSettings => 'Nastavenia kontaktov'; + + @override + String get settings_contactSettingsSubtitle => + 'Nastavenia pre pridávanie kontaktov.'; + @override String get settings_privacyMode => 'Režim ochrany súkromia'; @@ -1493,6 +1503,13 @@ class AppLocalizationsSk extends AppLocalizations { @override String get map_showSharedMarkers => 'Zobraziť zdieľané značky'; + @override + String get map_showGuessedLocations => + 'Zobraziť umiestnenia odhadnutých uzlov'; + + @override + String get map_guessedLocation => 'Odhadnutá lokalita'; + @override String get map_lastSeenTime => 'Posledný čas sledovania'; @@ -3173,4 +3190,82 @@ class AppLocalizationsSk extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Naposledy videný'; + + @override + String get contactsSettings_title => 'Nastavenia kontaktov'; + + @override + String get contactsSettings_autoAddTitle => 'Automatické zisťovanie'; + + @override + String get contactsSettings_otherTitle => + 'Ďalšie nastavenia súvisiace s kontaktami'; + + @override + String get contactsSettings_autoAddUsersTitle => + 'Automaticky pridávať užívateľov'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Povoliť spoločníkovi automaticky pridávať objavených užívateľov.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => + 'Automaticky pridávať opakovače'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Povoliť spoločníkovi automaticky pridávať objavené repeater.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Automaticky pridávať server miestnosti'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Povoliť spoločníkovi automaticky pridať objavené serverové miestnosti.'; + + @override + String get contactsSettings_autoAddSensorsTitle => + 'Automaticky pridávať senzory'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Povoliť spoločníkovi automaticky pridávať objavené senzory.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Prepísať najstaršie'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'Keď je zoznam kontaktov plný, bude nahradený najstarší neoznačený kontakt.'; + + @override + String get discoveredContacts_Title => 'Objavené kontakty'; + + @override + String get discoveredContacts_noMatching => 'Žiadne zhodné kontakty'; + + @override + String get discoveredContacts_searchHint => 'Vyhľadať objavené kontakty'; + + @override + String get discoveredContacts_contactAdded => 'Kontakt bol pridaný'; + + @override + String get discoveredContacts_addContact => 'Pridať kontakt'; + + @override + String get discoveredContacts_copyContact => 'Kopírovať kontakt do schránky'; + + @override + String get discoveredContacts_deleteContact => 'Zmazať kontakt'; + + @override + String get discoveredContacts_deleteContactAll => + 'Zmazať všetky objavené kontakty'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Ste si istí, že chcete zmazať všetky objavené kontakty?'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index d8d1d53..8c501d3 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -38,6 +38,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get common_delete => 'Izbrisati'; + @override + String get common_deleteAll => 'Izbriši vse'; + @override String get common_close => 'Zapri'; @@ -304,6 +307,13 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_longitude => 'Dolžina'; + @override + String get settings_contactSettings => 'Nastavitve stika'; + + @override + String get settings_contactSettingsSubtitle => + 'Nastavitve za dodajanje stikov.'; + @override String get settings_privacyMode => 'Zasebnost'; @@ -1485,6 +1495,12 @@ class AppLocalizationsSl extends AppLocalizations { @override String get map_showSharedMarkers => 'Pokaži skupno označenja'; + @override + String get map_showGuessedLocations => 'Pokaži lokacije domnevnih not.'; + + @override + String get map_guessedLocation => 'Predpostavljena lokacija'; + @override String get map_lastSeenTime => 'Datum zadnjega vpogleda'; @@ -3174,4 +3190,81 @@ class AppLocalizationsSl extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Zadnjič videno'; + + @override + String get contactsSettings_title => 'Nastavitve stikov'; + + @override + String get contactsSettings_autoAddTitle => 'Avtomatsko odkrivanje'; + + @override + String get contactsSettings_otherTitle => 'Druge nastavitve v zvezi s stiki'; + + @override + String get contactsSettings_autoAddUsersTitle => + 'Avtomatsko dodaj uporabnike'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Dovoli spremljevalcu, da samodejno doda odkrite uporabnike.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => + 'Avtomatsko dodaj ponovitelje'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Dovoli spremljevalcu, da samodejno doda odkrite ponovitelje.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Avtomatsko dodaj strežnike sob'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Dovoli spremljevalcu, da samodejno doda odkrite strežnike sob.'; + + @override + String get contactsSettings_autoAddSensorsTitle => + 'Avtomatsko dodaj senzorje'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Dovoli spremljevalcu, da samodejno doda odkrite senzorje.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Prepiši najstarejše'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'Ko je seznam stikov poln, bo najstarejši nestarševski stik zamenjan.'; + + @override + String get discoveredContacts_Title => 'Odkriti stiki'; + + @override + String get discoveredContacts_noMatching => 'Ni ujemajočih stikov'; + + @override + String get discoveredContacts_searchHint => 'Najdeni stiki po iskanju'; + + @override + String get discoveredContacts_contactAdded => 'Kontakt dodan'; + + @override + String get discoveredContacts_addContact => 'Dodaj stik'; + + @override + String get discoveredContacts_copyContact => 'Kopiraj stik v odložišče'; + + @override + String get discoveredContacts_deleteContact => 'Izbriši stik'; + + @override + String get discoveredContacts_deleteContactAll => + 'Izbriši vse odkrite kontakte'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Ste prepričani, da želite izbrisati vse odkrite kontakte?'; } diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index e36fc2a..6cff1ec 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -38,6 +38,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get common_delete => 'Radera'; + @override + String get common_deleteAll => 'Ta bort alla'; + @override String get common_close => 'Stänga'; @@ -304,6 +307,13 @@ class AppLocalizationsSv extends AppLocalizations { @override String get settings_longitude => 'Längdgrad'; + @override + String get settings_contactSettings => 'Kontaktinställningar'; + + @override + String get settings_contactSettingsSubtitle => + 'Inställningar för hur kontakter läggs till.'; + @override String get settings_privacyMode => 'Privatläge'; @@ -1482,6 +1492,13 @@ class AppLocalizationsSv extends AppLocalizations { @override String get map_showSharedMarkers => 'Visa delade markörer'; + @override + String get map_showGuessedLocations => + 'Visa upp de antagna nodernas placeringar'; + + @override + String get map_guessedLocation => 'Gissad plats'; + @override String get map_lastSeenTime => 'Senaste Visats Tid'; @@ -3153,4 +3170,82 @@ class AppLocalizationsSv extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Senast sedd'; + + @override + String get contactsSettings_title => 'Kontaktinställningar'; + + @override + String get contactsSettings_autoAddTitle => 'Automatisk upptäckt'; + + @override + String get contactsSettings_otherTitle => + 'Andra inställningar relaterade till kontakt'; + + @override + String get contactsSettings_autoAddUsersTitle => + 'Lägg till användare automatiskt'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Tillåt kompanjonen att automatiskt lägga till upptäckta användare'; + + @override + String get contactsSettings_autoAddRepeatersTitle => + 'Lägg till upprepande enheter automatiskt'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Tillåt kompanjonen att automatiskt lägga till upptäckta repeater.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Lägg automatiskt till rumsservrar'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Tillåt kompanjonen att automatiskt lägga till upptäckta rumsservrar.'; + + @override + String get contactsSettings_autoAddSensorsTitle => + 'Lägg till sensorer automatiskt'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Tillåt kompanjonen att automatiskt lägga till upptäckta sensorer.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Skriv över äldst'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'När kontaktlistan är full ersätts den äldsta icke-favoriterade kontakten.'; + + @override + String get discoveredContacts_Title => 'Upptäckta kontakter'; + + @override + String get discoveredContacts_noMatching => 'Inga matchande kontakter'; + + @override + String get discoveredContacts_searchHint => 'Sök uppfunna kontakter'; + + @override + String get discoveredContacts_contactAdded => 'Kontakt tillagd'; + + @override + String get discoveredContacts_addContact => 'Lägg till kontakt'; + + @override + String get discoveredContacts_copyContact => 'Kopiera kontakt till urklipp'; + + @override + String get discoveredContacts_deleteContact => 'Ta bort kontakt'; + + @override + String get discoveredContacts_deleteContactAll => + 'Ta bort alla upptäckta kontakter'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Är du säker på att du vill ta bort alla upptäckta kontakter?'; } diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index fe6ca55..7b5901f 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -38,6 +38,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get common_delete => 'Видалити'; + @override + String get common_deleteAll => 'Видалити все'; + @override String get common_close => 'Закрити'; @@ -305,6 +308,13 @@ class AppLocalizationsUk extends AppLocalizations { @override String get settings_longitude => 'Довгота'; + @override + String get settings_contactSettings => 'Налаштування контактів'; + + @override + String get settings_contactSettingsSubtitle => + 'Налаштування для додавання контактів'; + @override String get settings_privacyMode => 'Режим приватності'; @@ -1497,6 +1507,13 @@ class AppLocalizationsUk extends AppLocalizations { @override String get map_showSharedMarkers => 'Показувати спільні маркери'; + @override + String get map_showGuessedLocations => + 'Показати місцезнаходження передбачених вузлів'; + + @override + String get map_guessedLocation => 'Визначено місцезнаходження'; + @override String get map_lastSeenTime => 'Час останньої активності'; @@ -3203,4 +3220,84 @@ class AppLocalizationsUk extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Останній раз бачили'; + + @override + String get contactsSettings_title => 'Налаштування контактів'; + + @override + String get contactsSettings_autoAddTitle => 'Автоматичне виявлення'; + + @override + String get contactsSettings_otherTitle => + 'Інші налаштування, пов\'язані з контактами'; + + @override + String get contactsSettings_autoAddUsersTitle => + 'Автоматично додавати користувачів'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Дозволити супутникові автоматично додавати виявлених користувачів'; + + @override + String get contactsSettings_autoAddRepeatersTitle => + 'Автоматично додавати повторювачі'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Дозволити супутнику автоматично додавати виявлені ретранслятори'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Автоматично додавати сервери кімнат'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Дозволити супровіднику автоматично додавати виявлені сервери кімнат.'; + + @override + String get contactsSettings_autoAddSensorsTitle => + 'Автоматично додавати датчики'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Дозволити супровіднику автоматично додавати виявлені сенсори'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Перезаписати найстаріше'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'Коли список контактів заповнений, найстарший контакт без позначки улюбленого буде замінений.'; + + @override + String get discoveredContacts_Title => 'Виявлені контакти'; + + @override + String get discoveredContacts_noMatching => + 'Відповідних контактів не знайдено'; + + @override + String get discoveredContacts_searchHint => 'Знайти виявлені контакти'; + + @override + String get discoveredContacts_contactAdded => 'Контакт додано'; + + @override + String get discoveredContacts_addContact => 'Додати контакт'; + + @override + String get discoveredContacts_copyContact => + 'Копіювати контакт у буфер обміну'; + + @override + String get discoveredContacts_deleteContact => 'Видалити контакт'; + + @override + String get discoveredContacts_deleteContactAll => + 'Видалити всі виявлені контакти'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Ви впевнені, що хочете видалити всі виявлені контакти?'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 9b32209..9e26c50 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -38,6 +38,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get common_delete => '删除'; + @override + String get common_deleteAll => '删除全部'; + @override String get common_close => '关闭'; @@ -290,6 +293,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get settings_longitude => '经度'; + @override + String get settings_contactSettings => '联系人设置'; + + @override + String get settings_contactSettingsSubtitle => '添加联系人的设置'; + @override String get settings_privacyMode => '隐私模式'; @@ -1411,6 +1420,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get map_showSharedMarkers => '显示共享标记'; + @override + String get map_showGuessedLocations => '显示猜测的节点位置'; + + @override + String get map_guessedLocation => '猜测的位置'; + @override String get map_lastSeenTime => '最后在线时间'; @@ -2954,4 +2969,71 @@ class AppLocalizationsZh extends AppLocalizations { @override String get snrIndicator_lastSeen => '最近访问'; + + @override + String get contactsSettings_title => '联系人设置'; + + @override + String get contactsSettings_autoAddTitle => '自动发现'; + + @override + String get contactsSettings_otherTitle => '其他联系人相关设置'; + + @override + String get contactsSettings_autoAddUsersTitle => '自动添加用户'; + + @override + String get contactsSettings_autoAddUsersSubtitle => '允许伴侣自动添加发现的用户'; + + @override + String get contactsSettings_autoAddRepeatersTitle => '自动添加重复器'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => '允许伴侣自动添加发现的重复器'; + + @override + String get contactsSettings_autoAddRoomServersTitle => '自动添加房间服务器'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => '允许伴侣自动添加发现的房间服务器'; + + @override + String get contactsSettings_autoAddSensorsTitle => '自动添加传感器'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => '允许伴侣自动添加发现的传感器'; + + @override + String get contactsSettings_overwriteOldestTitle => '覆盖最旧的'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + '当联系人列表已满时,将替换最老的非收藏联系人。'; + + @override + String get discoveredContacts_Title => '已发现的联系人'; + + @override + String get discoveredContacts_noMatching => '没有匹配的联系人'; + + @override + String get discoveredContacts_searchHint => '搜索已发现的联系人'; + + @override + String get discoveredContacts_contactAdded => '联系人已添加'; + + @override + String get discoveredContacts_addContact => '添加联系人'; + + @override + String get discoveredContacts_copyContact => '复制联系人到剪贴板'; + + @override + String get discoveredContacts_deleteContact => '删除联系人'; + + @override + String get discoveredContacts_deleteContactAll => '删除所有发现的联系人'; + + @override + String get discoveredContacts_deleteContactAllContent => '您确定要删除所有发现的联系人吗?'; } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index ba22eb2..cde7457 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1802,23 +1802,31 @@ "contacts_searchUsers": "Zoek {number}{str} gebruikers...", "contacts_searchFavorites": "Zoek {number}{str} favorieten...", "contacts_searchRoomServers": "Zoek {number}{str} Room servers...", - "usbScreenTitle": "Verbind via USB", - "usbScreenStatus": "Selecteer een USB-apparaat", - "usbScreenNote": "USB-serieel is actief op ondersteunde Android-apparaten en desktop-platforms.", - "usbScreenSubtitle": "Kies een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.", - "usbScreenEmptyState": "Geen USB-apparaten gevonden. Sluit er een aan en herlaad.", - "usbErrorPermissionDenied": "Toegang via USB is geweigerd.", - "usbErrorDeviceMissing": "Het geselecteerde USB-apparaat is niet meer beschikbaar.", - "usbErrorInvalidPort": "Selecteer een geldig USB-apparaat.", - "usbErrorBusy": "Een andere verzoek om een USB-verbinding is al in behandeling.", - "usbErrorNotConnected": "Er is geen USB-apparaat aangesloten.", - "usbErrorOpenFailed": "Kon het geselecteerde USB-apparaat niet openen.", - "usbErrorConnectFailed": "Kon niet verbinding maken met het geselecteerde USB-apparaat.", - "usbErrorUnsupported": "USB-serieel is niet ondersteund op deze platform.", - "usbErrorAlreadyActive": "Een USB-verbinding is al actief.", - "usbErrorNoDeviceSelected": "Geen USB-apparaat is geselecteerd.", - "usbErrorPortClosed": "De USB-verbinding is niet actief.", - "usbErrorConnectTimedOut": "Wachtperiode is verlopen, er is geen reactie ontvangen van het apparaat.", - "connectionChoiceUsbLabel": "USB", - "connectionChoiceBluetoothLabel": "Bluetooth" + "contactsSettings_autoAddUsersTitle": "Gebruikers automatisch toevoegen", + "contactsSettings_title": "Instellingen voor contacten", + "settings_contactSettings": "Contactinstellingen", + "contactsSettings_otherTitle": "Andere instellingen voor contactgerelateerde zaken", + "contactsSettings_autoAddRepeatersSubtitle": "Sta toe dat de companion automatisch ontdekte repeaters toevoegt", + "contactsSettings_autoAddRoomServersTitle": "Automatisch kamerservers toevoegen", + "contactsSettings_autoAddRoomServersSubtitle": "Sta toe dat de companion automatisch ontdekte kamer servers toevoegt.", + "contactsSettings_autoAddSensorsTitle": "Automatisch sensoren toevoegen", + "settings_contactSettingsSubtitle": "Instellingen voor het toevoegen van contacten", + "contactsSettings_autoAddTitle": "Automatische detectie", + "contactsSettings_autoAddSensorsSubtitle": "Sta toe dat de companion automatisch ontdekte sensoren toevoegt", + "contactsSettings_autoAddUsersSubtitle": "Sta toe dat de companion automatisch ontdekte gebruikers toevoegt", + "contactsSettings_autoAddRepeatersTitle": "Automatisch herhalingstoestellen toevoegen", + "contactsSettings_overwriteOldestTitle": "Overschrijf Oudste", + "discoveredContacts_noMatching": "Geen overeenkomende contacten", + "discoveredContacts_addContact": "Contact toevoegen", + "discoveredContacts_copyContact": "Kopieer contact naar klembord", + "discoveredContacts_deleteContact": "Contact verwijderen", + "discoveredContacts_Title": "Ontdekte contacten", + "discoveredContacts_contactAdded": "Contact toegevoegd", + "discoveredContacts_searchHint": "Ontdekte contacten zoeken", + "contactsSettings_overwriteOldestSubtitle": "Wanneer de contactenlijst vol is, wordt de oudste niet-favoriete contactpersoon vervangen.", + "common_deleteAll": "Alles verwijderen", + "discoveredContacts_deleteContactAll": "Verwijder alle ontdekte contacten", + "discoveredContacts_deleteContactAllContent": "Weet u zeker dat u alle ontdekte contacten wilt verwijderen?", + "map_guessedLocation": "Geroerde locatie", + "map_showGuessedLocations": "Toon de voorspelde locaties van de knopen" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 97e3321..41152a5 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1802,23 +1802,31 @@ "contacts_searchRoomServers": "Wyszukaj {number}{str} serwerów Room...", "contacts_searchUsers": "Wyszukaj {number}{str} Użytkowników...", "contacts_searchRepeaters": "Wyszukaj {number}{str} powtórników...", - "usbScreenStatus": "Wybierz urządzenie USB", - "usbScreenTitle": "Połącz przez USB", - "usbScreenNote": "Port szeregowy USB jest aktywny na urządzeniach z Androidem i platformach stacjonarnych, które obsługują tę funkcję.", - "usbScreenSubtitle": "Wybierz wykryty urządzenie szeregowe i podłącz je bezpośrednio do swojego węzła MeshCore.", - "usbScreenEmptyState": "Nie znaleziono żadnych urządzeń USB. Podłącz jedno i zaktualizuj.", - "usbErrorPermissionDenied": "Zostało odrzucone żądanie dostępu przez USB.", - "usbErrorDeviceMissing": "Wybór urządzenia USB już nie jest dostępny.", - "usbErrorInvalidPort": "Wybierz prawidłowe urządzenie USB.", - "usbErrorBusy": "Kolejne żądanie połączenia przez USB jest już w trakcie realizacji.", - "usbErrorNotConnected": "Brak podłączonego urządzenia USB.", - "usbErrorOpenFailed": "Nie udało się otworzyć wybranego urządzenia USB.", - "usbErrorConnectFailed": "Nie udało się nawiązać połączenia z wybranym urządzeniem USB.", - "usbErrorUnsupported": "Port szeregowy USB nie jest obsługiwany na tym urządzeniu.", - "usbErrorAlreadyActive": "Połączenie USB jest już aktywne.", - "usbErrorNoDeviceSelected": "Nie został wybrany żaden urządzenie USB.", - "usbErrorPortClosed": "Połączenie USB nie jest aktywne.", - "usbErrorConnectTimedOut": "Czekanie na odpowiedź urządzenia zakończyło się z powodu braku reakcji.", - "connectionChoiceUsbLabel": "USB", - "connectionChoiceBluetoothLabel": "Bluetooth" + "contactsSettings_title": "Ustawienia kontaktów", + "settings_contactSettingsSubtitle": "Ustawienia dotyczące sposobu dodawania kontaktów", + "contactsSettings_autoAddUsersSubtitle": "Pozwól towarzyszowi automatycznie dodawać znalezione użytkowników.", + "contactsSettings_autoAddRepeatersTitle": "Automatyczne dodawanie powtarzalników", + "contactsSettings_autoAddRepeatersSubtitle": "Zezwól na automatyczne dodawanie odkrytych repeaterów przez towarzysza.", + "contactsSettings_autoAddRoomServersTitle": "Automatycznie dodaj serwery pokojowe", + "contactsSettings_autoAddUsersTitle": "Automatycznie dodaj użytkowników", + "settings_contactSettings": "Ustawienia kontaktowe", + "contactsSettings_otherTitle": "Inne ustawienia związane z kontaktami", + "contactsSettings_autoAddTitle": "Automatyczne odnajdywanie", + "contactsSettings_autoAddRoomServersSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie znalezionych serwerów pokojowych.", + "contactsSettings_autoAddSensorsTitle": "Automatycznie dodaj czujniki", + "discoveredContacts_searchHint": "Wyszukaj odkryte kontakty", + "discoveredContacts_contactAdded": "Kontakt dodany", + "discoveredContacts_addContact": "Dodaj kontakt", + "discoveredContacts_copyContact": "Kopiuj kontakt do schowka", + "contactsSettings_overwriteOldestTitle": "Nadpisz najstarszy", + "discoveredContacts_Title": "Odkryte Kontakty", + "contactsSettings_autoAddSensorsSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie wykrytych czujników.", + "discoveredContacts_noMatching": "Brak pasujących kontaktów", + "discoveredContacts_deleteContact": "Usuń kontakt", + "contactsSettings_overwriteOldestSubtitle": "Gdy lista kontaktów jest pełna, najstarszy nieulubiony kontakt zostanie zastąpiony.", + "common_deleteAll": "Usuń wszystko", + "discoveredContacts_deleteContactAllContent": "Czy na pewno chcesz usunąć wszystkie znalezione kontakty?", + "discoveredContacts_deleteContactAll": "Usuń wszystkie odkryte kontakty", + "map_guessedLocation": "Wydana lokalizacja", + "map_showGuessedLocations": "Wyświetl lokalizacje zgadanych węzłów" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 6636e8f..f843119 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1802,23 +1802,31 @@ "contacts_searchContactsNoNumber": "Pesquisar Contatos...", "contacts_unread": "Não lido", "contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala...", - "usbScreenTitle": "Conecte via USB", - "usbScreenSubtitle": "Selecione o dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.", - "usbScreenStatus": "Selecione um dispositivo USB", - "usbScreenNote": "A comunicação serial via USB está ativa em dispositivos Android e plataformas de desktop compatíveis.", - "usbScreenEmptyState": "Nenhum dispositivo USB encontrado. Conecte um e atualize.", - "usbErrorPermissionDenied": "A permissão para acesso via USB foi negada.", - "usbErrorDeviceMissing": "O dispositivo USB selecionado não está mais disponível.", - "usbErrorInvalidPort": "Selecione um dispositivo USB válido.", - "usbErrorBusy": "Já existe uma solicitação de conexão USB em andamento.", - "usbErrorNotConnected": "Não há nenhum dispositivo USB conectado.", - "usbErrorOpenFailed": "Não foi possível abrir o dispositivo USB selecionado.", - "usbErrorConnectFailed": "Não foi possível conectar ao dispositivo USB selecionado.", - "usbErrorUnsupported": "A comunicação serial via USB não é suportada nesta plataforma.", - "usbErrorAlreadyActive": "A conexão USB já está ativa.", - "usbErrorNoDeviceSelected": "Nenhum dispositivo USB foi selecionado.", - "usbErrorPortClosed": "A conexão USB não está ativa.", - "usbErrorConnectTimedOut": "Tempo limite aguardando a resposta do dispositivo.", - "connectionChoiceUsbLabel": "USB", - "connectionChoiceBluetoothLabel": "Bluetooth" + "settings_contactSettings": "Configurações de Contato", + "contactsSettings_otherTitle": "Outras configurações relacionadas a contatos", + "contactsSettings_title": "Configurações de contatos", + "contactsSettings_autoAddTitle": "Descoberta Automática", + "settings_contactSettingsSubtitle": "Configurações para como os contatos são adicionados", + "contactsSettings_autoAddUsersTitle": "Adicionar usuários automaticamente", + "contactsSettings_autoAddRepeatersSubtitle": "Permitir que o companheiro adicione automaticamente os repetidores descobertos.", + "contactsSettings_autoAddRoomServersTitle": "Adicionar automaticamente servidores de sala", + "contactsSettings_overwriteOldestTitle": "Sobrescrever o Mais Antigo", + "contactsSettings_autoAddSensorsTitle": "Adicionar sensores automaticamente", + "discoveredContacts_Title": "Contatos Descobertos", + "contactsSettings_autoAddUsersSubtitle": "Permitir que o companheiro adicione automaticamente os usuários descobertos.", + "contactsSettings_autoAddRepeatersTitle": "Adicionar repetidores automaticamente", + "discoveredContacts_noMatching": "Nenhum contato correspondente", + "contactsSettings_autoAddRoomServersSubtitle": "Permitir que o companheiro adicione automaticamente os servidores de salas descobertos.", + "discoveredContacts_searchHint": "Pesquisar contatos descobertos", + "contactsSettings_autoAddSensorsSubtitle": "Permitir que o companheiro adicione automaticamente sensores descobertos.", + "discoveredContacts_copyContact": "Copiar Contato para a área de transferência", + "discoveredContacts_deleteContact": "Excluir Contato", + "discoveredContacts_contactAdded": "Contato adicionado", + "discoveredContacts_addContact": "Adicionar Contato", + "contactsSettings_overwriteOldestSubtitle": "Quando a lista de contatos estiver cheia, o contato mais antigo não favoritado será substituído.", + "common_deleteAll": "Excluir Tudo", + "discoveredContacts_deleteContactAll": "Excluir Todos os Contatos Descobertos", + "discoveredContacts_deleteContactAllContent": "Tem certeza de que deseja excluir todos os contatos descobertos?", + "map_guessedLocation": "Localização estimada", + "map_showGuessedLocations": "Mostrar as localizações dos nós estimados" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index bfbee95..0897cba 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1042,23 +1042,31 @@ "contacts_searchRoomServers": "Поиск {number}{str} серверов комнат...", "contacts_searchFavorites": "Поиск {number}{str} избранного...", "contacts_searchUsers": "Поиск {number}{str} пользователей...", - "usbScreenSubtitle": "Выберите обнаруженное устройство с последовательным интерфейсом и подключите его напрямую к вашему узлу MeshCore.", - "usbScreenTitle": "Подключение через USB", - "usbScreenStatus": "Выберите USB-устройство", - "usbScreenNote": "USB-серийный порт активен на поддерживаемых устройствах Android и на настольных платформах.", - "usbScreenEmptyState": "Не обнаружено устройств USB. Подключите одно из них и обновите список.", - "usbErrorPermissionDenied": "Запрос на доступ через USB был отклонен.", - "usbErrorDeviceMissing": "Выбранное USB-устройство больше недоступно.", - "usbErrorInvalidPort": "Выберите действительное USB-устройство.", - "usbErrorBusy": "Еще одно запрошенное соединение через USB уже находится в процессе.", - "usbErrorNotConnected": "Ни одно устройство USB не подключено.", - "usbErrorOpenFailed": "Не удалось открыть выбранное USB-устройство.", - "usbErrorConnectFailed": "Не удалось установить соединение с выбранным USB-устройством.", - "usbErrorUnsupported": "Поддержка последовательного USB отсутствует на данной платформе.", - "usbErrorAlreadyActive": "USB-соединение уже установлено и работает.", - "usbErrorNoDeviceSelected": "Не было выбрано ни одно устройство USB.", - "usbErrorPortClosed": "USB-соединение не установлено.", - "usbErrorConnectTimedOut": "Ожидание ответа от устройства превысило установленное время.", - "connectionChoiceBluetoothLabel": "Bluetooth", - "connectionChoiceUsbLabel": "USB" + "settings_contactSettings": "Настройки контактов", + "settings_contactSettingsSubtitle": "Настройки добавления контактов", + "contactsSettings_autoAddTitle": "Автоматическое обнаружение", + "contactsSettings_title": "Настройки контактов", + "contactsSettings_otherTitle": "Другие настройки, связанные с контактами", + "contactsSettings_autoAddUsersSubtitle": "Разрешить компаньону автоматически добавлять обнаруженных пользователей", + "contactsSettings_autoAddRoomServersTitle": "Автоматически добавлять серверы комнат", + "contactsSettings_autoAddSensorsTitle": "Автоматически добавлять датчики", + "contactsSettings_autoAddSensorsSubtitle": "Разрешить компаньону автоматически добавлять обнаруженные датчики", + "contactsSettings_autoAddUsersTitle": "Автоматически добавлять пользователей", + "contactsSettings_overwriteOldestTitle": "Перезаписать самое старое", + "contactsSettings_autoAddRepeatersTitle": "Автоматически добавлять ретрансляторы", + "contactsSettings_autoAddRepeatersSubtitle": "Разрешить спутнику автоматически добавлять обнаруженные ретрансляторы", + "contactsSettings_autoAddRoomServersSubtitle": "Разрешить компаньону автоматически добавлять обнаруженные сервера комнат.", + "discoveredContacts_noMatching": "Нет совпадающих контактов", + "discoveredContacts_searchHint": "Найденные контакты поиска", + "discoveredContacts_contactAdded": "Контакт добавлен", + "discoveredContacts_copyContact": "Копировать контакт в буфер обмена", + "discoveredContacts_addContact": "Добавить контакт", + "discoveredContacts_Title": "Обнаруженные контакты", + "discoveredContacts_deleteContact": "Удалить контакт", + "contactsSettings_overwriteOldestSubtitle": "Когда список контактов заполнен, будет заменен самый старый контакт, который не находится в избранном.", + "common_deleteAll": "Удалить все", + "discoveredContacts_deleteContactAllContent": "Вы уверены, что хотите удалить все обнаруженные контакты?", + "discoveredContacts_deleteContactAll": "Удалить Все Обнаруженные Контакты", + "map_guessedLocation": "Угаданное место", + "map_showGuessedLocations": "Отобразить предполагаемые места расположения узлов" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 1269fe1..16aae9b 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1802,23 +1802,31 @@ "contacts_searchUsers": "Hľadať {number}{str} používateľov...", "contacts_searchContactsNoNumber": "Hľadať kontakty...", "contacts_unread": "Neprečítané", - "usbScreenStatus": "Vyberte USB zariadenie", - "usbScreenTitle": "Pripojte cez USB", - "usbScreenSubtitle": "Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vašej MeshCore uzlu.", - "usbScreenNote": "USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.", - "usbScreenEmptyState": "Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte.", - "usbErrorPermissionDenied": "Žiadosť o prístup cez USB bola zamietnutá.", - "usbErrorDeviceMissing": "Vybrané USB zariadenie už nie je dostupné.", - "usbErrorInvalidPort": "Vyberte platné USB zariadenie.", - "usbErrorBusy": "Ďalšia požiadavka na pripojenie cez USB je aktuálne v prebiehajúcom procese.", - "usbErrorNotConnected": "Nie je pripojené žiadne USB zariadenie.", - "usbErrorOpenFailed": "Nepodarilo sa otvoriť vybrané USB zariadenie.", - "usbErrorConnectFailed": "Nezvládlo sa pripojenie k vybranému USB zariadeniu.", - "usbErrorUnsupported": "Podpora USB sériového rozhrania nie je na tejto platforme dostupná.", - "usbErrorAlreadyActive": "Pripojenie cez USB je už aktivované.", - "usbErrorNoDeviceSelected": "Nebolo vybrané žiadne USB zariadenie.", - "usbErrorPortClosed": "Pripojenie cez USB nie je aktivované.", - "usbErrorConnectTimedOut": "Čakal som, kým sa zariadenie neozvými, ale časový limit sa dosiahol.", - "connectionChoiceUsbLabel": "USB", - "connectionChoiceBluetoothLabel": "Bluetooth" + "settings_contactSettingsSubtitle": "Nastavenia pre pridávanie kontaktov.", + "contactsSettings_autoAddUsersTitle": "Automaticky pridávať užívateľov", + "contactsSettings_autoAddUsersSubtitle": "Povoliť spoločníkovi automaticky pridávať objavených užívateľov.", + "contactsSettings_autoAddRepeatersTitle": "Automaticky pridávať opakovače", + "contactsSettings_autoAddRoomServersTitle": "Automaticky pridávať server miestnosti", + "contactsSettings_autoAddRoomServersSubtitle": "Povoliť spoločníkovi automaticky pridať objavené serverové miestnosti.", + "contactsSettings_autoAddTitle": "Automatické zisťovanie", + "contactsSettings_title": "Nastavenia kontaktov", + "contactsSettings_otherTitle": "Ďalšie nastavenia súvisiace s kontaktami", + "settings_contactSettings": "Nastavenia kontaktov", + "contactsSettings_autoAddSensorsTitle": "Automaticky pridávať senzory", + "discoveredContacts_noMatching": "Žiadne zhodné kontakty", + "discoveredContacts_searchHint": "Vyhľadať objavené kontakty", + "contactsSettings_autoAddRepeatersSubtitle": "Povoliť spoločníkovi automaticky pridávať objavené repeater.", + "discoveredContacts_contactAdded": "Kontakt bol pridaný", + "discoveredContacts_copyContact": "Kopírovať kontakt do schránky", + "discoveredContacts_deleteContact": "Zmazať kontakt", + "contactsSettings_autoAddSensorsSubtitle": "Povoliť spoločníkovi automaticky pridávať objavené senzory.", + "discoveredContacts_Title": "Objavené kontakty", + "contactsSettings_overwriteOldestTitle": "Prepísať najstaršie", + "discoveredContacts_addContact": "Pridať kontakt", + "contactsSettings_overwriteOldestSubtitle": "Keď je zoznam kontaktov plný, bude nahradený najstarší neoznačený kontakt.", + "discoveredContacts_deleteContactAll": "Zmazať všetky objavené kontakty", + "common_deleteAll": "Zmazať všetko", + "discoveredContacts_deleteContactAllContent": "Ste si istí, že chcete zmazať všetky objavené kontakty?", + "map_showGuessedLocations": "Zobraziť umiestnenia odhadnutých uzlov", + "map_guessedLocation": "Odhadnutá lokalita" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index ec09ee1..97d913f 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1802,23 +1802,31 @@ "contacts_searchContactsNoNumber": "Iskanje stikov...", "contacts_searchRepeaters": "Išči {number}{str} ponavljalnike...", "contacts_searchUsers": "Išči {number}{str} uporabnikov...", - "usbScreenStatus": "Izberite USB naprave.", - "usbScreenSubtitle": "Izberite zaznano serijsko napravo in se neposredno povežite z vašo MeshCore napravo.", - "usbScreenTitle": "Povežite preko USB", - "usbScreenNote": "USB serijska povezava je aktivna na podprtih napravah Android in na desktop platformah.", - "usbScreenEmptyState": "Niti en USB naprave niso najdeni. Povežite eno in posodobite.", - "usbErrorPermissionDenied": "Dovoljenje za dostop preko USB-ja je bilo zavrnjeno.", - "usbErrorDeviceMissing": "Izbrani USB napravega več ni na voljo.", - "usbErrorInvalidPort": "Izberite veljavno USB naprave.", - "usbErrorBusy": "Že je v teku zahteva za povezavo preko USB.", - "usbErrorNotConnected": "Ni priklopljenih USB naprav.", - "usbErrorOpenFailed": "Niso uspeli odkriti izbrane USB naprave.", - "usbErrorConnectFailed": "Niso bilo mogoče uskladiti povezave z izbranim USB napom.", - "usbErrorUnsupported": "USB serijska komunikacija ni podprta na tej platformi.", - "usbErrorAlreadyActive": "USB povezava je že aktivirana.", - "usbErrorNoDeviceSelected": "Ni bilo izbranega USB naprave.", - "usbErrorPortClosed": "USB povezava ni aktivirana.", - "usbErrorConnectTimedOut": "Čakanje je preseglo določeno časovno obdobo, ker se naprave ni odzval.", - "connectionChoiceBluetoothLabel": "Bluetooth", - "connectionChoiceUsbLabel": "USB" + "settings_contactSettings": "Nastavitve stika", + "contactsSettings_autoAddTitle": "Avtomatsko odkrivanje", + "contactsSettings_autoAddUsersTitle": "Avtomatsko dodaj uporabnike", + "contactsSettings_autoAddRepeatersTitle": "Avtomatsko dodaj ponovitelje", + "contactsSettings_autoAddRepeatersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite ponovitelje.", + "contactsSettings_autoAddRoomServersTitle": "Avtomatsko dodaj strežnike sob", + "contactsSettings_autoAddRoomServersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite strežnike sob.", + "contactsSettings_otherTitle": "Druge nastavitve v zvezi s stiki", + "settings_contactSettingsSubtitle": "Nastavitve za dodajanje stikov.", + "contactsSettings_title": "Nastavitve stikov", + "contactsSettings_autoAddSensorsTitle": "Avtomatsko dodaj senzorje", + "contactsSettings_autoAddUsersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite uporabnike.", + "discoveredContacts_noMatching": "Ni ujemajočih stikov", + "contactsSettings_autoAddSensorsSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite senzorje.", + "discoveredContacts_addContact": "Dodaj stik", + "discoveredContacts_contactAdded": "Kontakt dodan", + "discoveredContacts_copyContact": "Kopiraj stik v odložišče", + "contactsSettings_overwriteOldestTitle": "Prepiši najstarejše", + "discoveredContacts_Title": "Odkriti stiki", + "discoveredContacts_searchHint": "Najdeni stiki po iskanju", + "discoveredContacts_deleteContact": "Izbriši stik", + "contactsSettings_overwriteOldestSubtitle": "Ko je seznam stikov poln, bo najstarejši nestarševski stik zamenjan.", + "common_deleteAll": "Izbriši vse", + "discoveredContacts_deleteContactAllContent": "Ste prepričani, da želite izbrisati vse odkrite kontakte?", + "discoveredContacts_deleteContactAll": "Izbriši vse odkrite kontakte", + "map_guessedLocation": "Predpostavljena lokacija", + "map_showGuessedLocations": "Pokaži lokacije domnevnih not." } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 836bef0..b451082 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1802,23 +1802,31 @@ "contacts_searchFavorites": "Sök {number}{str} Favoriter...", "contacts_searchUsers": "Sök {number}{str} användare...", "contacts_searchRoomServers": "Sök {number}{str} Room-servrar...", - "usbScreenNote": "USB-seriell kommunikation är aktiv på stödda Android-enheter och skrivbordsplattformar.", - "usbScreenStatus": "Välj en USB-enhet", - "usbScreenSubtitle": "Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.", - "usbScreenTitle": "Anslut via USB", - "usbScreenEmptyState": "Inga USB-enheter hittades. Anslut en och uppdatera.", - "usbErrorPermissionDenied": "Tillgången via USB nekas.", - "usbErrorDeviceMissing": "Den valda USB-enheten är inte längre tillgänglig.", - "usbErrorInvalidPort": "Välj en giltig USB-enhet.", - "usbErrorBusy": "En annan förfrågan om USB-anslutning är redan pågående.", - "usbErrorNotConnected": "Ingen USB-enhet är ansluten.", - "usbErrorOpenFailed": "Kunde inte öppna det valda USB-enheten.", - "usbErrorConnectFailed": "Kunde inte ansluta till det valda USB-enheten.", - "usbErrorUnsupported": "USB-seriell kommunikation stöds inte på denna plattform.", - "usbErrorAlreadyActive": "En USB-anslutning är redan aktiv.", - "usbErrorNoDeviceSelected": "Ingen USB-enhet valdes.", - "usbErrorPortClosed": "USB-anslutningen är inte aktiv.", - "usbErrorConnectTimedOut": "Tiden har löpt ut medan vi väntade på att enheten skulle svara.", - "connectionChoiceBluetoothLabel": "Bluetooth", - "connectionChoiceUsbLabel": "USB" + "settings_contactSettingsSubtitle": "Inställningar för hur kontakter läggs till.", + "settings_contactSettings": "Kontaktinställningar", + "contactsSettings_autoAddTitle": "Automatisk upptäckt", + "contactsSettings_otherTitle": "Andra inställningar relaterade till kontakt", + "contactsSettings_autoAddUsersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta användare", + "contactsSettings_autoAddRepeatersTitle": "Lägg till upprepande enheter automatiskt", + "contactsSettings_autoAddRoomServersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta rumsservrar.", + "contactsSettings_autoAddSensorsTitle": "Lägg till sensorer automatiskt", + "contactsSettings_autoAddUsersTitle": "Lägg till användare automatiskt", + "contactsSettings_title": "Kontaktinställningar", + "contactsSettings_autoAddSensorsSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta sensorer.", + "contactsSettings_overwriteOldestTitle": "Skriv över äldst", + "contactsSettings_autoAddRepeatersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta repeater.", + "contactsSettings_autoAddRoomServersTitle": "Lägg automatiskt till rumsservrar", + "discoveredContacts_noMatching": "Inga matchande kontakter", + "discoveredContacts_searchHint": "Sök uppfunna kontakter", + "discoveredContacts_deleteContact": "Ta bort kontakt", + "discoveredContacts_Title": "Upptäckta kontakter", + "discoveredContacts_contactAdded": "Kontakt tillagd", + "discoveredContacts_addContact": "Lägg till kontakt", + "discoveredContacts_copyContact": "Kopiera kontakt till urklipp", + "contactsSettings_overwriteOldestSubtitle": "När kontaktlistan är full ersätts den äldsta icke-favoriterade kontakten.", + "common_deleteAll": "Ta bort alla", + "discoveredContacts_deleteContactAllContent": "Är du säker på att du vill ta bort alla upptäckta kontakter?", + "discoveredContacts_deleteContactAll": "Ta bort alla upptäckta kontakter", + "map_guessedLocation": "Gissad plats", + "map_showGuessedLocations": "Visa upp de antagna nodernas placeringar" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 66cdf64..fb60b81 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1802,23 +1802,31 @@ "contacts_searchContactsNoNumber": "Пошук контактів...", "contacts_searchRepeaters": "Пошук {number}{str} ретрансляторів...", "contacts_unread": "Непрочитане", - "usbScreenNote": "USB-серіальний порт активний на підтримуваних пристроях на базі Android та на десктопних платформах.", - "usbScreenSubtitle": "Виберіть виявлене серійне пристрій і підключіть його безпосередньо до вашого вузла MeshCore.", - "usbScreenStatus": "Виберіть пристрій USB", - "usbScreenTitle": "Підключити через USB", - "usbScreenEmptyState": "Не знайдено жодних пристроїв USB. Підключіть один і перезавантажте.", - "usbErrorPermissionDenied": "Було відмовлено у наданні дозволу на використання USB.", - "usbErrorDeviceMissing": "Вибране USB-пристрій більше недоступне.", - "usbErrorInvalidPort": "Виберіть дійсний USB-пристрій.", - "usbErrorBusy": "Ще один запит на підключення через USB вже обробляється.", - "usbErrorNotConnected": "Немає підключених пристроїв USB.", - "usbErrorOpenFailed": "Не вдалося відкрити вибране USB-пристрій.", - "usbErrorConnectFailed": "Не вдалося підключитися до вибраного USB-пристрою.", - "usbErrorUnsupported": "Підтримка USB-серіального інтерфейсу не реалізована на цій платформі.", - "usbErrorAlreadyActive": "USB-з'єднання вже встановлено.", - "usbErrorNoDeviceSelected": "Не було вибрано жодного пристрою USB.", - "usbErrorPortClosed": "З'єднання USB не встановлено.", - "usbErrorConnectTimedOut": "Час очікування закінчився, оскільки пристрій не відповів.", - "connectionChoiceBluetoothLabel": "Bluetooth", - "connectionChoiceUsbLabel": "USB" + "settings_contactSettingsSubtitle": "Налаштування для додавання контактів", + "settings_contactSettings": "Налаштування контактів", + "contactsSettings_autoAddUsersSubtitle": "Дозволити супутникові автоматично додавати виявлених користувачів", + "contactsSettings_autoAddRepeatersTitle": "Автоматично додавати повторювачі", + "contactsSettings_autoAddRepeatersSubtitle": "Дозволити супутнику автоматично додавати виявлені ретранслятори", + "contactsSettings_autoAddRoomServersTitle": "Автоматично додавати сервери кімнат", + "contactsSettings_otherTitle": "Інші налаштування, пов'язані з контактами", + "contactsSettings_autoAddTitle": "Автоматичне виявлення", + "contactsSettings_autoAddUsersTitle": "Автоматично додавати користувачів", + "contactsSettings_title": "Налаштування контактів", + "contactsSettings_autoAddRoomServersSubtitle": "Дозволити супровіднику автоматично додавати виявлені сервери кімнат.", + "contactsSettings_autoAddSensorsTitle": "Автоматично додавати датчики", + "discoveredContacts_searchHint": "Знайти виявлені контакти", + "discoveredContacts_contactAdded": "Контакт додано", + "contactsSettings_autoAddSensorsSubtitle": "Дозволити супровіднику автоматично додавати виявлені сенсори", + "contactsSettings_overwriteOldestTitle": "Перезаписати найстаріше", + "discoveredContacts_Title": "Виявлені контакти", + "discoveredContacts_noMatching": "Відповідних контактів не знайдено", + "discoveredContacts_deleteContact": "Видалити контакт", + "discoveredContacts_copyContact": "Копіювати контакт у буфер обміну", + "discoveredContacts_addContact": "Додати контакт", + "contactsSettings_overwriteOldestSubtitle": "Коли список контактів заповнений, найстарший контакт без позначки улюбленого буде замінений.", + "common_deleteAll": "Видалити все", + "discoveredContacts_deleteContactAll": "Видалити всі виявлені контакти", + "discoveredContacts_deleteContactAllContent": "Ви впевнені, що хочете видалити всі виявлені контакти?", + "map_showGuessedLocations": "Показати місцезнаходження передбачених вузлів", + "map_guessedLocation": "Визначено місцезнаходження" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 07af01e..51bd60c 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1807,23 +1807,31 @@ "contacts_searchContactsNoNumber": "搜索联系人...", "contacts_searchRoomServers": "搜索 {number}{str} 房间服务器...", "contacts_searchFavorites": "搜索 {number}{str} 收藏...", - "usbScreenTitle": "通过USB连接", - "usbScreenSubtitle": "选择已检测到的串行设备,并直接连接到您的 MeshCore 节点。", - "usbScreenNote": "在支持的 Android 设备和桌面平台上,USB 串行通信功能已启用。", - "usbScreenStatus": "选择一个 USB 设备", - "usbScreenEmptyState": "未找到任何 USB 设备。请插入一个,然后刷新。", - "usbErrorPermissionDenied": "拒绝了USB权限。", - "usbErrorDeviceMissing": "所选的USB设备已不再可用。", - "usbErrorInvalidPort": "选择一个有效的USB设备。", - "usbErrorBusy": "还有一个 USB 连接请求正在进行中。", - "usbErrorNotConnected": "没有连接任何USB设备。", - "usbErrorOpenFailed": "未能打开所选的USB设备。", - "usbErrorConnectFailed": "未能连接到所选的USB设备。", - "usbErrorUnsupported": "此平台不支持USB串行通信。", - "usbErrorAlreadyActive": "USB 连接已建立。", - "usbErrorNoDeviceSelected": "未选择任何 USB 设备。", - "usbErrorPortClosed": "USB 连接未建立。", - "usbErrorConnectTimedOut": "等待设备响应超时。", - "connectionChoiceBluetoothLabel": "蓝牙", - "connectionChoiceUsbLabel": "USB" + "settings_contactSettings": "联系人设置", + "contactsSettings_title": "联系人设置", + "contactsSettings_autoAddUsersTitle": "自动添加用户", + "contactsSettings_otherTitle": "其他联系人相关设置", + "contactsSettings_autoAddUsersSubtitle": "允许伴侣自动添加发现的用户", + "contactsSettings_autoAddRepeatersSubtitle": "允许伴侣自动添加发现的重复器", + "contactsSettings_autoAddSensorsTitle": "自动添加传感器", + "contactsSettings_autoAddRoomServersSubtitle": "允许伴侣自动添加发现的房间服务器", + "contactsSettings_autoAddRepeatersTitle": "自动添加重复器", + "contactsSettings_autoAddTitle": "自动发现", + "settings_contactSettingsSubtitle": "添加联系人的设置", + "contactsSettings_overwriteOldestTitle": "覆盖最旧的", + "contactsSettings_autoAddSensorsSubtitle": "允许伴侣自动添加发现的传感器", + "discoveredContacts_searchHint": "搜索已发现的联系人", + "contactsSettings_autoAddRoomServersTitle": "自动添加房间服务器", + "discoveredContacts_contactAdded": "联系人已添加", + "discoveredContacts_deleteContact": "删除联系人", + "discoveredContacts_addContact": "添加联系人", + "discoveredContacts_noMatching": "没有匹配的联系人", + "discoveredContacts_Title": "已发现的联系人", + "discoveredContacts_copyContact": "复制联系人到剪贴板", + "contactsSettings_overwriteOldestSubtitle": "当联系人列表已满时,将替换最老的非收藏联系人。", + "common_deleteAll": "删除全部", + "discoveredContacts_deleteContactAllContent": "您确定要删除所有发现的联系人吗?", + "discoveredContacts_deleteContactAll": "删除所有发现的联系人", + "map_showGuessedLocations": "显示猜测的节点位置", + "map_guessedLocation": "猜测的位置" } diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index 62ba9ca..abcc729 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -22,6 +22,7 @@ class AppSettings { final bool mapKeyPrefixEnabled; final String mapKeyPrefix; final bool mapShowMarkers; + final bool mapShowGuessedLocations; final bool enableMessageTracing; final Map? mapCacheBounds; final int mapCacheMinZoom; @@ -48,6 +49,7 @@ class AppSettings { this.mapKeyPrefixEnabled = false, this.mapKeyPrefix = '', this.mapShowMarkers = true, + this.mapShowGuessedLocations = true, this.enableMessageTracing = false, this.mapCacheBounds, this.mapCacheMinZoom = 10, @@ -78,6 +80,7 @@ class AppSettings { 'map_key_prefix_enabled': mapKeyPrefixEnabled, 'map_key_prefix': mapKeyPrefix, 'map_show_markers': mapShowMarkers, + 'map_show_guessed_locations': mapShowGuessedLocations, 'enable_message_tracing': enableMessageTracing, 'map_cache_bounds': mapCacheBounds, 'map_cache_min_zoom': mapCacheMinZoom, @@ -115,6 +118,8 @@ class AppSettings { mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false, mapKeyPrefix: json['map_key_prefix'] as String? ?? '', mapShowMarkers: json['map_show_markers'] as bool? ?? true, + mapShowGuessedLocations: + json['map_show_guessed_locations'] as bool? ?? true, enableMessageTracing: json['enable_message_tracing'] as bool? ?? false, mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map( (key, value) => MapEntry(key.toString(), (value as num).toDouble()), @@ -159,6 +164,7 @@ class AppSettings { bool? mapKeyPrefixEnabled, String? mapKeyPrefix, bool? mapShowMarkers, + bool? mapShowGuessedLocations, bool? enableMessageTracing, Object? mapCacheBounds = _unset, int? mapCacheMinZoom, @@ -185,6 +191,8 @@ class AppSettings { mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled, mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix, mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers, + mapShowGuessedLocations: + mapShowGuessedLocations ?? this.mapShowGuessedLocations, enableMessageTracing: enableMessageTracing ?? this.enableMessageTracing, mapCacheBounds: mapCacheBounds == _unset ? this.mapCacheBounds diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 5e532e6..7d8e011 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -183,7 +183,7 @@ class Contact { ) : Uint8List(0); final name = readCString(data, contactNameOffset, maxNameSize); - final lastmod = readUint32LE(data, contactLastmodOffset); + final lastmod = readUint32LE(data, contactLastModOffset); double? lat, lon; final latRaw = readInt32LE(data, contactLatOffset); diff --git a/lib/models/discovery_contact.dart b/lib/models/discovery_contact.dart new file mode 100644 index 0000000..f6c6a52 --- /dev/null +++ b/lib/models/discovery_contact.dart @@ -0,0 +1,105 @@ +import 'dart:typed_data'; +import '../connector/meshcore_protocol.dart'; + +class DiscoveryContact { + final Uint8List rawPacket; + final Uint8List publicKey; + final String name; + final int type; + final int pathLength; // -1 = flood, 0+ = direct hops (from device) + final Uint8List path; // Path bytes from device + final double? latitude; + final double? longitude; + final DateTime lastSeen; + + DiscoveryContact({ + required this.rawPacket, + required this.publicKey, + required this.name, + required this.type, + required this.pathLength, + required this.path, + this.latitude, + this.longitude, + required this.lastSeen, + }); + + String get publicKeyHex => pubKeyToHex(publicKey); + + String get typeLabel { + switch (type) { + case advTypeChat: + return 'Chat'; + case advTypeRepeater: + return 'Repeater'; + case advTypeRoom: + return 'Room'; + case advTypeSensor: + return 'Sensor'; + default: + return 'Unknown'; + } + } + + String get pathLabel { + if (pathLength < 0) return 'Flood'; + if (pathLength == 0) return 'Direct'; + return '$pathLength hops'; + } + + bool get hasLocation => latitude != null && longitude != null; + + DiscoveryContact copyWith({ + Uint8List? rawPacket, + Uint8List? publicKey, + String? name, + int? type, + int? pathLength, + Uint8List? path, + double? latitude, + double? longitude, + DateTime? lastSeen, + }) { + return DiscoveryContact( + rawPacket: rawPacket ?? this.rawPacket, + publicKey: publicKey ?? this.publicKey, + name: name ?? this.name, + type: type ?? this.type, + pathLength: pathLength ?? this.pathLength, + path: path ?? this.path, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + lastSeen: lastSeen ?? this.lastSeen, + ); + } + + String get pathIdList { + final pathBytes = path; + if (pathBytes.isEmpty) return ''; + final parts = []; + final groupSize = pathHashSize; + for (int i = 0; i < pathBytes.length; i += groupSize) { + final end = (i + groupSize) <= pathBytes.length + ? (i + groupSize) + : pathBytes.length; + final chunk = pathBytes.sublist(i, end); + parts.add( + chunk + .map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()) + .join(), + ); + } + return parts.join(','); + } + + String get shortPubKeyHex { + return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>"; + } + + @override + bool operator ==(Object other) => + other is DiscoveryContact && publicKeyHex == other.publicKeyHex; + + @override + int get hashCode => publicKeyHex.hashCode; +} diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 7c8fcfb..0075040 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -818,6 +818,7 @@ class _ChatScreenState extends State { title: context.l10n.contacts_repeaterPathTrace, path: Uint8List.fromList(pathBytes), flipPathRound: true, + targetContact: widget.contact, ), ), ), diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 8c4cec4..3fef9ec 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -26,6 +26,7 @@ import '../widgets/room_login_dialog.dart'; import '../widgets/unread_badge.dart'; import 'channels_screen.dart'; import 'chat_screen.dart'; +import 'discovery_screen.dart'; import 'map_screen.dart'; import 'repeater_hub_screen.dart'; import 'settings_screen.dart'; @@ -218,9 +219,10 @@ class _ContactsScreenState extends State } final hexString = text.substring('meshcore://'.length); try { - final importContactFrame = buildImportContactFrame(hexString); + final bytes = hex2Uint8List(hexString); + final importContactFrame = buildImportContactFrame(bytes); _pendingOperations.add(ContactOperationType.import); - await connector.sendFrame(importContactFrame, expectsGenericAck: true); + connector.importContact(importContactFrame); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -318,6 +320,21 @@ class _ContactsScreenState extends State ), onTap: () => _disconnect(context, connector), ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.person_add_rounded), + const SizedBox(width: 8), + Text("Discovered Contacts"), + ], + ), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const DiscoveryScreen(), + ), + ), + ), PopupMenuItem( child: Row( children: [ @@ -1121,6 +1138,7 @@ class _ContactsScreenState extends State contact.name, ), path: contact.traceRouteBytes ?? Uint8List(0), + targetContact: contact, ), ), ); diff --git a/lib/screens/discovery_screen.dart b/lib/screens/discovery_screen.dart new file mode 100644 index 0000000..f122654 --- /dev/null +++ b/lib/screens/discovery_screen.dart @@ -0,0 +1,419 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; + +import '../connector/meshcore_connector.dart'; +import '../connector/meshcore_protocol.dart'; +import '../l10n/l10n.dart'; +import '../models/discovery_contact.dart'; +import '../utils/contact_search.dart'; +import '../widgets/app_bar.dart'; +import '../widgets/list_filter_widget.dart'; + +enum DiscoverySortOption { lastSeen, name, type } + +class DiscoveryScreen extends StatefulWidget { + const DiscoveryScreen({super.key}); + + @override + State createState() => _DiscoveryScreenState(); +} + +class _DiscoveryScreenState extends State { + final TextEditingController _searchController = TextEditingController(); + String searchQuery = ''; + ContactSortOption sortOption = ContactSortOption.lastSeen; + bool showUnreadOnly = false; + ContactTypeFilter typeFilter = ContactTypeFilter.all; + DiscoverySortOption discoverySortOption = DiscoverySortOption.lastSeen; + Timer? _searchDebounce; + + @override + void dispose() { + _searchController.dispose(); + _searchDebounce?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final connector = context.watch(); + + final discoveredContacts = connector.discoveredContacts; + final filteredAndSorted = _filterAndSortContacts( + discoveredContacts, + connector, + ); + + return Scaffold( + appBar: AppBar( + title: AppBarTitle( + l10n.discoveredContacts_Title, + indicators: false, + subtitle: false, + ), + centerTitle: true, + actions: [ + PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.delete, color: Colors.red), + const SizedBox(width: 8), + Text(context.l10n.discoveredContacts_deleteContactAll), + ], + ), + onTap: () { + _deleteContacts(context, connector); + }, + ), + ], + icon: const Icon(Icons.more_vert), + ), + ], + ), + body: Column( + children: [ + _buildFilters(filteredAndSorted, connector), + Expanded( + child: discoveredContacts.isEmpty + ? Center(child: Text(l10n.contacts_noContacts)) + : filteredAndSorted.isEmpty + ? Center(child: Text(l10n.discoveredContacts_noMatching)) + : ListView.builder( + itemCount: filteredAndSorted.length, + itemBuilder: (context, index) { + final contact = filteredAndSorted[index]; + return ListTile( + leading: CircleAvatar( + backgroundColor: _getTypeColor(contact.type), + child: Icon( + _getTypeIcon(contact.type), + color: Colors.white, + size: 20, + ), + ), + title: Text( + contact.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text( + contact.shortPubKeyHex, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + trailing: Text( + _formatLastSeen(context, contact.lastSeen), + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + onTap: () { + connector.importDiscoveredContact(contact); + }, + onLongPress: () => + _showContactContextMenu(contact, connector), + ); + }, + ), + ), + ], + ), + ); + } + + Future _showContactContextMenu( + DiscoveryContact contact, + MeshCoreConnector connector, + ) async { + final action = await showModalBottomSheet( + context: context, + showDragHandle: true, + builder: (sheetContext) { + final l10n = context.l10n; + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.add_reaction_sharp), + title: Text(l10n.discoveredContacts_addContact), + onTap: () => Navigator.of(sheetContext).pop('import_contact'), + ), + ListTile( + leading: const Icon(Icons.copy), + title: Text(l10n.discoveredContacts_copyContact), + onTap: () => Navigator.of(sheetContext).pop('copy_contact'), + ), + ListTile( + leading: const Icon(Icons.delete), + title: Text(l10n.discoveredContacts_deleteContact), + onTap: () => Navigator.of(sheetContext).pop('delete_contact'), + ), + ], + ), + ); + }, + ); + + if (!mounted || action == null) return; + + switch (action) { + case 'import_contact': + connector.importDiscoveredContact(contact); + break; + case 'copy_contact': + final hexString = pubKeyToHex(contact.rawPacket); + Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)), + ); + break; + case 'delete_contact': + connector.removeDiscoveredContact(contact); + break; + } + } + + void _deleteContacts(BuildContext context, MeshCoreConnector connector) { + final l10n = context.l10n; + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(l10n.common_deleteAll), + content: Text(l10n.discoveredContacts_deleteContactAllContent), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(l10n.common_cancel), + ), + TextButton( + onPressed: () async { + Navigator.pop(context); + connector.removeAllDiscoveredContacts(); + }, + child: Text(l10n.common_deleteAll), + ), + ], + ), + ); + } + + Widget _buildFilters( + List filteredAndSorted, + MeshCoreConnector connector, + ) { + String hintText = ""; + switch (typeFilter) { + case ContactTypeFilter.all: + hintText = context.l10n.contacts_searchContacts( + filteredAndSorted.length, + showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + ); + break; + case ContactTypeFilter.users: + hintText = context.l10n.contacts_searchUsers( + filteredAndSorted.length, + showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + ); + break; + case ContactTypeFilter.repeaters: + hintText = context.l10n.contacts_searchRepeaters( + filteredAndSorted.length, + showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + ); + break; + case ContactTypeFilter.rooms: + hintText = context.l10n.contacts_searchRoomServers( + filteredAndSorted.length, + showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + ); + break; + case ContactTypeFilter.favorites: + hintText = context.l10n.contacts_searchFavorites( + filteredAndSorted.length, + showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + ); + break; + } + + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: hintText, + prefixIcon: const Icon(Icons.search), + suffixIcon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (searchQuery.isNotEmpty) + IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + _searchController.clear(); + setState(() { + searchQuery = ''; + }); + }, + ), + _buildFilterButton(context, connector), + ], + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + onChanged: (value) { + _searchDebounce?.cancel(); + _searchDebounce = Timer(const Duration(milliseconds: 300), () { + if (!mounted) return; + setState(() { + searchQuery = value.toLowerCase(); + }); + }); + }, + ), + ), + ], + ); + } + + Widget _buildFilterButton(BuildContext context, MeshCoreConnector connector) { + return DiscoveryContactsFilterMenu( + sortOption: sortOption, + typeFilter: typeFilter, + onSortChanged: (value) { + setState(() { + sortOption = value; + }); + }, + onTypeFilterChanged: (value) { + setState(() { + typeFilter = value; + }); + }, + ); + } + + List _filterAndSortContacts( + List contacts, + MeshCoreConnector connector, + ) { + var filtered = contacts.where((contact) { + if (searchQuery.isEmpty) return true; + return matchesDiscoveryContactQuery(contact, searchQuery); + }).toList(); + + filtered = filtered.where((contact) { + return !connector.knownContactKeys.contains(contact.publicKeyHex); + }).toList(); + + // Filter out own node from the list + if (connector.selfPublicKey != null) { + final selfPubKeyHex = pubKeyToHex(connector.selfPublicKey!); + filtered = filtered.where((contact) { + return contact.publicKeyHex != selfPubKeyHex; + }).toList(); + } + + if (typeFilter != ContactTypeFilter.all) { + filtered = filtered.where(_matchesTypeFilter).toList(); + } + + switch (sortOption) { + case ContactSortOption.lastSeen: + filtered.sort((a, b) => b.lastSeen.compareTo(a.lastSeen)); + break; + case ContactSortOption.name: + filtered.sort( + (a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()), + ); + break; + default: + break; + } + + return filtered; + } + + bool _matchesTypeFilter(DiscoveryContact contact) { + switch (typeFilter) { + case ContactTypeFilter.all: + return true; + case ContactTypeFilter.users: + return contact.type == advTypeChat; + case ContactTypeFilter.repeaters: + return contact.type == advTypeRepeater; + case ContactTypeFilter.rooms: + return contact.type == advTypeRoom; + default: + return false; + } + } + + IconData _getTypeIcon(int type) { + switch (type) { + case advTypeChat: + return Icons.chat; + case advTypeRepeater: + return Icons.cell_tower; + case advTypeRoom: + return Icons.group; + case advTypeSensor: + return Icons.sensors; + default: + return Icons.device_unknown; + } + } + + Color _getTypeColor(int type) { + switch (type) { + case advTypeChat: + return Colors.blue; + case advTypeRepeater: + return Colors.orange; + case advTypeRoom: + return Colors.purple; + case advTypeSensor: + return Colors.green; + default: + return Colors.grey; + } + } + + String _formatLastSeen(BuildContext context, DateTime lastSeen) { + final now = DateTime.now(); + final diff = now.difference(lastSeen); + + 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); + } +} diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 2ec71a0..3d94701 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -15,6 +15,7 @@ import '../models/app_settings.dart'; import '../models/channel.dart'; import '../models/contact.dart'; import '../services/app_settings_service.dart'; +import '../services/path_history_service.dart'; import '../services/map_marker_service.dart'; import '../services/map_tile_cache_service.dart'; import '../utils/contact_search.dart'; @@ -64,6 +65,8 @@ class _MapScreenState extends State { final List _polylines = []; bool _legendExpanded = false; bool _showNodeLabels = true; + List<_GuessedLocation> _cachedGuessedLocations = []; + String _guessedLocationsCacheKey = ''; @override void initState() { @@ -119,8 +122,8 @@ class _MapScreenState extends State { @override Widget build(BuildContext context) { - return Consumer2( - builder: (context, connector, settingsService, child) { + return Consumer3( + builder: (context, connector, settingsService, pathHistory, child) { final tileCache = context.read(); final settings = settingsService.settings; final contacts = connector.contacts; @@ -160,6 +163,40 @@ class _MapScreenState extends State { .where((c) => c.hasLocation) .toList(); + // All contacts with a known location — used as anchors regardless of + // time/key-prefix filters so that repeaters are always available. + final allContactsWithLocation = contacts + .where((c) => c.hasLocation) + .toList(); + + // Compute guessed locations with caching + final maxRangeKm = _estimateLoRaRangeKm(connector); + final filteredKeys = filteredByKeyPrefix + .map((c) => '${c.publicKeyHex}:${c.path.join("-")}') + .join(','); + final anchorKeys = allContactsWithLocation + .map( + (c) => + '${c.publicKeyHex}:${c.latitude}:${c.longitude}:${c.path.isNotEmpty ? c.path.last : ""}', + ) + .join(','); + final cacheKey = + '$filteredKeys|$anchorKeys|${pathHistory.version}:${connector.currentSf}:${connector.currentBwHz}:${connector.currentTxPower}:${settings.mapShowGuessedLocations}'; + if (cacheKey != _guessedLocationsCacheKey) { + _guessedLocationsCacheKey = cacheKey; + _cachedGuessedLocations = settings.mapShowGuessedLocations + ? _computeGuessedLocations( + filteredByKeyPrefix, + allContactsWithLocation, + pathHistory, + maxRangeKm, + ) + : []; + } + final guessedLocations = settings.mapShowGuessedLocations + ? _cachedGuessedLocations + : <_GuessedLocation>[]; + _polylines.clear(); _polylines.addAll( _points.length > 1 @@ -430,6 +467,8 @@ class _MapScreenState extends State { size: 34, ), ), + if (!_isBuildingPathTrace) + ...guessedLocations.map(_buildGuessedMarker), ..._buildMarkers( contactsWithLocation, settings, @@ -489,6 +528,7 @@ class _MapScreenState extends State { contactsWithLocation, settings, sharedMarkers.length, + guessedLocations.length, ), if (_isBuildingPathTrace) _buildPathTraceOverlay(), ], @@ -512,6 +552,200 @@ class _MapScreenState extends State { ); } + List<_GuessedLocation> _computeGuessedLocations( + List allContacts, + List withLocation, + PathHistoryService pathHistory, + double? maxRangeKm, + ) { + // Index known-location repeaters by their 1-byte hash. + // null value = two repeaters share the same hash byte (ambiguous collision). + final repeaterByHash = {}; + for (final c in withLocation) { + if (c.type == advTypeRepeater) { + if (repeaterByHash.containsKey(c.publicKey[0])) { + repeaterByHash[c.publicKey[0]] = + null; // collision: can't disambiguate + } else { + repeaterByHash[c.publicKey[0]] = c; + } + } + } + + final result = <_GuessedLocation>[]; + + for (final contact in allContacts) { + if (contact.hasLocation) continue; + + final anchorSet = {}; + + // Collect the contact-side (last-hop) repeater from every known path. + // path = [device-side hop, ..., contact-side hop] + // Only path.last is actually within radio range of the contact — using + // earlier bytes would anchor against our own side of the network. + final pathSets = >[ + contact.path.toList(), + ...pathHistory + .getRecentPaths(contact.publicKeyHex) + .map((r) => r.pathBytes), + ]; + final lastHopBytes = {}; + for (final pathBytes in pathSets) { + if (pathBytes.isEmpty) continue; + final lastHop = pathBytes.last; + lastHopBytes.add(lastHop); + final r = repeaterByHash[lastHop]; + if (r != null) anchorSet.add(LatLng(r.latitude!, r.longitude!)); + } + + // Fallback: for any last-hop byte with no GPS repeater, average the + // positions of contacts with known GPS that share the same last hop. + // Those contacts are all adjacent to the same unknown repeater, so their + // centroid is a reasonable proxy for its location. + for (final byte in lastHopBytes) { + if (repeaterByHash.containsKey(byte)) continue; + for (final c in withLocation) { + if (c.path.isNotEmpty && c.path.last == byte) { + anchorSet.add(LatLng(c.latitude!, c.longitude!)); + } + } + } + + // Filter anchors that are geometrically inconsistent with radio range. + // Two anchors more than 2 * maxRange apart cannot both be in direct radio + // range of the same node, so isolated outliers are removed. + final anchors = maxRangeKm != null && anchorSet.length > 1 + ? _filterConsistentAnchors(anchorSet.toList(), maxRangeKm) + : anchorSet.toList(); + + if (anchors.isEmpty) continue; + + final LatLng position; + if (anchors.length == 1) { + // Offset single-anchor guesses so they don't overlap the repeater marker. + // Use the contact's public key byte as a deterministic angle seed. + const offsetDeg = 0.003; // ~330 m at the equator + final angle = (contact.publicKey[1] / 255.0) * 2 * pi; + position = LatLng( + anchors[0].latitude + offsetDeg * cos(angle), + anchors[0].longitude + offsetDeg * sin(angle), + ); + } else { + double lat = 0, lon = 0; + for (final a in anchors) { + lat += a.latitude; + lon += a.longitude; + } + position = LatLng(lat / anchors.length, lon / anchors.length); + } + result.add( + _GuessedLocation( + contact: contact, + position: position, + highConfidence: anchors.length >= 2, + ), + ); + } + + return result; + } + + /// Estimates the free-space maximum LoRa range in km from the connected + /// device's current radio parameters. Returns null if parameters are unknown. + double? _estimateLoRaRangeKm(MeshCoreConnector connector) { + final freqHz = connector.currentFreqHz; + final bwHz = connector.currentBwHz; + final sf = connector.currentSf; + final txPower = connector.currentTxPower; + if (freqHz == null || bwHz == null || sf == null || txPower == null) { + return null; + } + // LoRa receiver sensitivity = thermal noise + NF + required demod SNR + const noiseFigureDb = 6.0; + final thermalNoiseDbm = -174.0 + 10 * log(bwHz.toDouble()) / ln10; + final sensitivityDbm = + thermalNoiseDbm + noiseFigureDb + _sfToRequiredSnrDb(sf); + // FSPL at max range equals link budget: + // FSPL = 20*log10(d_m) + 20*log10(f_hz) - 147.55 + final linkBudgetDb = txPower.toDouble() - sensitivityDbm; + final exponent = + (linkBudgetDb + 147.55 - 20 * log(freqHz.toDouble()) / ln10) / 20; + return pow(10, exponent) / 1000; + } + + double _sfToRequiredSnrDb(int sf) { + switch (sf) { + case 5: + return -2.5; + case 6: + return -5.0; + case 7: + return -7.5; + case 8: + return -10.0; + case 9: + return -12.5; + case 10: + return -15.0; + case 11: + return -17.5; + case 12: + return -20.0; + default: + return -10.0; + } + } + + /// Removes anchors that have no neighbour within 2 * maxRangeKm. + /// A node cannot be simultaneously in radio range of two points farther apart + /// than twice the expected maximum range. + List _filterConsistentAnchors( + List anchors, + double maxRangeKm, + ) { + const distance = Distance(); + final maxDistM = maxRangeKm * 2000; + return anchors + .where((a) => anchors.any((b) => b != a && distance(a, b) <= maxDistM)) + .toList(); + } + + Marker _buildGuessedMarker(_GuessedLocation guess) { + final color = _getNodeColor(guess.contact.type); + return Marker( + point: guess.position, + width: 35, + height: 35, + child: GestureDetector( + onTap: () => _showNodeInfo( + context, + guess.contact, + guessedPosition: guess.position, + ), + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: color.withValues(alpha: guess.highConfidence ? 0.55 : 0.30), + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: const Icon( + Icons.not_listed_location, + color: Colors.white, + size: 20, + ), + ), + ), + ); + } + List _buildMarkers( List contacts, settings, { @@ -657,6 +891,7 @@ class _MapScreenState extends State { List contactsWithLocation, settings, int markerCount, + int guessedCount, ) { int nodeCount = 0; for (final contact in contactsWithLocation) { @@ -696,7 +931,12 @@ class _MapScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - context.l10n.map_nodesCount(nodeCount), + context.l10n.map_nodesCount( + nodeCount + + (settings.mapShowGuessedLocations + ? guessedCount + : 0), + ), style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, @@ -764,6 +1004,12 @@ class _MapScreenState extends State { context.l10n.map_pinPublic, Colors.orange, ), + if (settings.mapShowGuessedLocations && guessedCount > 0) + _buildLegendItem( + Icons.not_listed_location, + context.l10n.map_guessedLocation, + Colors.grey, + ), ], ), ), @@ -952,7 +1198,11 @@ class _MapScreenState extends State { ); } - void _showNodeInfo(BuildContext context, Contact contact) { + void _showNodeInfo( + BuildContext context, + Contact contact, { + LatLng? guessedPosition, + }) { showDialog( context: context, builder: (dialogContext) => AlertDialog( @@ -972,10 +1222,16 @@ class _MapScreenState extends State { children: [ _buildInfoRow('Type', contact.typeLabel), _buildInfoRow('Path', contact.pathLabel), - _buildInfoRow( - 'Location', - '${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}', - ), + if (contact.hasLocation) + _buildInfoRow( + 'Location', + '${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}', + ) + else if (guessedPosition != null) + _buildInfoRow( + 'Est. Location', + '~${guessedPosition.latitude.toStringAsFixed(6)}, ${guessedPosition.longitude.toStringAsFixed(6)}', + ), _buildInfoRow( context.l10n.map_lastSeen, _formatLastSeen(contact.lastSeen), @@ -1481,6 +1737,14 @@ class _MapScreenState extends State { }, contentPadding: EdgeInsets.zero, ), + CheckboxListTile( + title: Text(context.l10n.map_showGuessedLocations), + value: settings.mapShowGuessedLocations, + onChanged: (value) { + service.setMapShowGuessedLocations(value ?? true); + }, + contentPadding: EdgeInsets.zero, + ), const SizedBox(height: 16), Text( context.l10n.map_keyPrefix, @@ -1744,6 +2008,18 @@ class _MapScreenState extends State { } } +class _GuessedLocation { + final Contact contact; + final LatLng position; + final bool highConfidence; + + _GuessedLocation({ + required this.contact, + required this.position, + required this.highConfidence, + }); +} + class _MarkerPayload { final LatLng position; final String label; diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 5f86cc1..c6d800e 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -54,6 +54,7 @@ class PathTraceMapScreen extends StatefulWidget { final int? repeaterId; final bool flipPathRound; final bool reversePathRound; + final Contact? targetContact; const PathTraceMapScreen({ super.key, @@ -62,6 +63,7 @@ class PathTraceMapScreen extends StatefulWidget { this.repeaterId, this.flipPathRound = false, this.reversePathRound = false, + this.targetContact, }); @override @@ -78,6 +80,11 @@ class _PathTraceMapScreenState extends State { bool _failed2Loaded = false; bool _hasData = false; PathTraceData? _traceData; + // Inferred positions for hops that have no GPS location, keyed by hop byte. + Map _inferredHopPositions = {}; + // Endpoint position for the target contact (GPS or guessed). + LatLng? _targetContactPosition; + bool _targetContactIsGuessed = false; List _points = []; List _polylines = []; LatLng? _initialCenter = LatLng(0, 0); @@ -242,25 +249,91 @@ class _PathTraceMapScreenState extends State { } }); + // For hops with no GPS contact, infer position from other contacts + // with known GPS that share the same last-hop byte. + final Map inferredPositions = {}; + for (final hop in pathData) { + final contact = pathContacts[hop]; + if (contact != null && contact.hasLocation) continue; + final peers = connector.contacts + .where( + (c) => c.hasLocation && c.path.isNotEmpty && c.path.last == hop, + ) + .toList(); + if (peers.isNotEmpty) { + final lat = + peers.map((c) => c.latitude!).reduce((a, b) => a + b) / + peers.length; + final lon = + peers.map((c) => c.longitude!).reduce((a, b) => a + b) / + peers.length; + inferredPositions[hop] = LatLng(lat, lon); + } + } + setState(() { _isLoading = false; _hasData = true; + _inferredHopPositions = inferredPositions; _traceData = PathTraceData( pathData: pathData, snrData: snrData, pathContacts: pathContacts, ); + // Compute endpoint position for the target contact. + LatLng? targetPos; + bool targetGuessed = false; + final target = widget.targetContact; + if (target != null) { + if (target.hasLocation) { + targetPos = LatLng(target.latitude!, target.longitude!); + } else if (pathData.isNotEmpty) { + // Infer from the last hop: average GPS contacts sharing that hop. + // For a round-trip path (flipPathRound), the target-side hop sits + // in the middle of the symmetric sequence; .last is the local side. + final lastHop = (widget.flipPathRound && pathData.length > 1) + ? pathData[(pathData.length - 1) ~/ 2] + : pathData.last; + final peers = connector.contacts + .where( + (c) => + c.hasLocation && + c.path.isNotEmpty && + c.path.last == lastHop, + ) + .toList(); + if (peers.isNotEmpty) { + final lat = + peers.map((c) => c.latitude!).reduce((a, b) => a + b) / + peers.length; + final lon = + peers.map((c) => c.longitude!).reduce((a, b) => a + b) / + peers.length; + const offsetDeg = 0.003; + final angle = (target.publicKey[1] / 255.0) * 2 * pi; + targetPos = LatLng( + lat + offsetDeg * cos(angle), + lon + offsetDeg * sin(angle), + ); + targetGuessed = true; + } + } + } + _targetContactPosition = targetPos; + _targetContactIsGuessed = targetGuessed; + _points = []; _points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!)); for (final hop in _traceData!.pathData) { final contact = _traceData!.pathContacts[hop]; - if (contact != null && - contact.hasLocation && - contact.latitude != null && - contact.longitude != null) { + if (contact != null && contact.hasLocation) { _points.add(LatLng(contact.latitude!, contact.longitude!)); + } else { + final inferred = inferredPositions[hop]; + if (inferred != null) _points.add(inferred); } } + if (targetPos != null) _points.add(targetPos); _polylines = _points.length > 1 ? [ Polyline( @@ -382,8 +455,13 @@ class _PathTraceMapScreenState extends State { final markers = []; for (final hop in pathData) { final contact = _traceData!.pathContacts[hop]; - if (contact == null || !contact.hasLocation) continue; - final point = LatLng(contact.latitude!, contact.longitude!); + final inferred = _inferredHopPositions[hop]; + final hasGps = contact != null && contact.hasLocation; + if (!hasGps && inferred == null) continue; + final point = hasGps + ? LatLng(contact.latitude!, contact.longitude!) + : inferred!; + final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase(); markers.add( Marker( point: point, @@ -392,7 +470,9 @@ class _PathTraceMapScreenState extends State { child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( - color: Colors.green, + color: hasGps + ? Colors.green + : Colors.orange.withValues(alpha: 0.75), shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), boxShadow: [ @@ -405,10 +485,7 @@ class _PathTraceMapScreenState extends State { ), alignment: Alignment.center, child: Text( - contact.publicKey - .sublist(0, 1) - .map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()) - .join(), + hasGps ? label : '~$label', style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, @@ -419,7 +496,12 @@ class _PathTraceMapScreenState extends State { ), ); if (showLabels) { - markers.add(_buildNodeLabelMarker(point: point, label: contact.name)); + markers.add( + _buildNodeLabelMarker( + point: point, + label: contact?.name ?? '~$label', + ), + ); } } @@ -468,6 +550,47 @@ class _PathTraceMapScreenState extends State { } } + // Add target contact endpoint marker. + final targetPos = _targetContactPosition; + if (targetPos != null) { + final isGuessed = _targetContactIsGuessed; + final targetName = widget.targetContact?.name ?? '?'; + markers.add( + Marker( + point: targetPos, + width: 35, + height: 35, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: isGuessed + ? Colors.purple.withValues(alpha: 0.55) + : Colors.red, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: const Icon(Icons.person, color: Colors.white, size: 18), + ), + ), + ); + if (showLabels) { + markers.add( + _buildNodeLabelMarker( + point: targetPos, + label: isGuessed ? '~$targetName' : targetName, + ), + ); + } + } + return markers; } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index a198f99..d6118f5 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -8,7 +8,7 @@ import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../l10n/l10n.dart'; import '../models/radio_settings.dart'; -import '../widgets/adaptive_app_bar_title.dart'; +import '../widgets/app_bar.dart'; import 'app_settings_screen.dart'; import 'app_debug_log_screen.dart'; import 'ble_debug_log_screen.dart'; @@ -43,8 +43,11 @@ class _SettingsScreenState extends State { final l10n = context.l10n; return Scaffold( appBar: AppBar( - title: AdaptiveAppBarTitle(l10n.settings_title), - centerTitle: true, + title: AppBarTitle( + l10n.settings_title, + indicators: false, + subtitle: false, + ), ), body: SafeArea( top: false, @@ -274,6 +277,14 @@ class _SettingsScreenState extends State { onTap: () => _editLocation(context, connector), ), const Divider(height: 1), + ListTile( + leading: const Icon(Icons.group_add_outlined), + title: Text(l10n.settings_contactSettings), + subtitle: Text(l10n.settings_contactSettingsSubtitle), + trailing: const Icon(Icons.chevron_right), + onTap: () => _editAutoAddConfig(context, connector), + ), + const Divider(height: 1), ListTile( leading: const Icon(Icons.visibility_off_outlined), title: Text(l10n.settings_privacyMode), @@ -849,6 +860,121 @@ class _SettingsScreenState extends State { ), ); } + + void _editAutoAddConfig(BuildContext context, MeshCoreConnector connector) { + final l10n = context.l10n; + bool autoAddChat = false; + bool autoAddRepeater = false; + bool autoAddRoomServer = false; + bool autoAddSensor = false; + bool overwriteOldest = false; + + final connector = context.read(); + autoAddChat = connector.autoAddUsers ?? false; + autoAddRepeater = connector.autoAddRepeaters ?? false; + autoAddRoomServer = connector.autoAddRoomServers ?? false; + autoAddSensor = connector.autoAddSensors ?? false; + overwriteOldest = connector.autoAddOverwriteOldest ?? false; + + showDialog( + context: context, + builder: (dialogContext) => StatefulBuilder( + builder: (context, setDialogState) => AlertDialog( + title: Text(l10n.contactsSettings_autoAddTitle), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + FeatureToggleRow( + title: l10n.contactsSettings_autoAddUsersTitle, + subtitle: l10n.contactsSettings_autoAddUsersSubtitle, + value: autoAddChat, + onChanged: (value) { + setDialogState(() => autoAddChat = value); + }, + ), + SizedBox(height: 8), + FeatureToggleRow( + title: l10n.contactsSettings_autoAddRepeatersTitle, + subtitle: l10n.contactsSettings_autoAddRepeatersSubtitle, + value: autoAddRepeater, + onChanged: (value) { + setDialogState(() => autoAddRepeater = value); + }, + ), + SizedBox(height: 8), + FeatureToggleRow( + title: l10n.contactsSettings_autoAddRoomServersTitle, + subtitle: l10n.contactsSettings_autoAddRoomServersSubtitle, + value: autoAddRoomServer, + onChanged: (value) { + setDialogState(() => autoAddRoomServer = value); + }, + ), + SizedBox(height: 8), + FeatureToggleRow( + title: l10n.contactsSettings_autoAddSensorsTitle, + subtitle: l10n.contactsSettings_autoAddSensorsSubtitle, + value: autoAddSensor, + onChanged: (value) { + setDialogState(() => autoAddSensor = value); + }, + ), + Divider(height: 4), + FeatureToggleRow( + title: l10n.contactsSettings_overwriteOldestTitle, + subtitle: l10n.contactsSettings_overwriteOldestSubtitle, + value: overwriteOldest, + onChanged: (value) { + setDialogState(() => overwriteOldest = value); + }, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(l10n.common_cancel), + ), + TextButton( + onPressed: () { + _sendSettings( + connector, + autoAddChat, + autoAddRepeater, + autoAddRoomServer, + autoAddSensor, + overwriteOldest, + ); + Navigator.pop(context); + }, + child: Text(l10n.common_save), + ), + ], + ), + ), + ); + } + + void _sendSettings( + MeshCoreConnector connector, + bool autoAddChat, + bool autoAddRepeater, + bool autoAddRoomServer, + bool autoAddSensor, + bool overwriteOldest, + ) async { + final frame = buildSetAutoAddConfigFrame( + autoAddChat: autoAddChat, + autoAddRepeater: autoAddRepeater, + autoAddRoomServer: autoAddRoomServer, + autoAddSensor: autoAddSensor, + overwriteOldest: overwriteOldest, + ); + await connector.sendFrame(frame); + await connector.sendFrame(buildGetAutoAddFlagsFrame()); + } } class _RadioSettingsDialog extends StatefulWidget { diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index eacf26f..c74fa40 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -80,6 +80,10 @@ class AppSettingsService extends ChangeNotifier { await updateSettings(_settings.copyWith(mapShowMarkers: value)); } + Future setMapShowGuessedLocations(bool value) async { + await updateSettings(_settings.copyWith(mapShowGuessedLocations: value)); + } + Future setEnableMessageTracing(bool value) async { await updateSettings(_settings.copyWith(enableMessageTracing: value)); } diff --git a/lib/services/ble_debug_log_service.dart b/lib/services/ble_debug_log_service.dart index bc46b59..df2822b 100644 --- a/lib/services/ble_debug_log_service.dart +++ b/lib/services/ble_debug_log_service.dart @@ -172,8 +172,6 @@ class BleDebugLogService extends ChangeNotifier { return 'CMD_GET_CHANNEL'; case cmdSetChannel: return 'CMD_SET_CHANNEL'; - case cmdGetRadioSettings: - return 'CMD_GET_RADIO_SETTINGS'; case cmdSetCustomVar: return 'CMD_SET_CUSTOM_VAR'; case cmdSendTracePath: @@ -215,8 +213,8 @@ class BleDebugLogService extends ChangeNotifier { return 'RESP_CODE_CHANNEL_MSG_RECV_V3'; case respCodeChannelInfo: return 'RESP_CODE_CHANNEL_INFO'; - case respCodeRadioSettings: - return 'RESP_CODE_RADIO_SETTINGS'; + case respCodeAutoAddConfig: + return 'RESP_CODE_AUTO_ADD_CONFIG'; case pushCodeTraceData: return 'PUSH_CODE_TRACE_DATA'; default: diff --git a/lib/services/path_history_service.dart b/lib/services/path_history_service.dart index 1314f48..569fada 100644 --- a/lib/services/path_history_service.dart +++ b/lib/services/path_history_service.dart @@ -15,6 +15,9 @@ class PathHistoryService extends ChangeNotifier { final List _cacheAccessOrder = []; static const int _maxHistoryEntries = 100; + + int _version = 0; + int get version => _version; static const int _autoRotationTopCount = 3; PathHistoryService(this._storage); @@ -185,6 +188,7 @@ class PathHistoryService extends ChangeNotifier { ) { var history = _cache[contactPubKeyHex]; if (history == null) return; + _version++; final existing = _findPathRecord(contactPubKeyHex, pathBytes); if (existing != null) { @@ -241,6 +245,7 @@ class PathHistoryService extends ChangeNotifier { _cache[contactPubKeyHex] = loaded; _trackAccess(contactPubKeyHex); _evictIfNeeded(); + _version++; notifyListeners(); } }); @@ -276,6 +281,7 @@ class PathHistoryService extends ChangeNotifier { _autoRotationIndex.remove(contactPubKeyHex); _floodStats.remove(contactPubKeyHex); await _storage.clearPathHistory(contactPubKeyHex); + _version++; notifyListeners(); } @@ -295,6 +301,7 @@ class PathHistoryService extends ChangeNotifier { ); await _storage.savePathHistory(contactPubKeyHex, _cache[contactPubKeyHex]!); + _version++; notifyListeners(); } diff --git a/lib/storage/contact_discovery_store.dart b/lib/storage/contact_discovery_store.dart new file mode 100644 index 0000000..37bfbb4 --- /dev/null +++ b/lib/storage/contact_discovery_store.dart @@ -0,0 +1,61 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import '../models/discovery_contact.dart'; +import 'prefs_manager.dart'; + +class ContactDiscoveryStore { + static const String _key = 'discovered_contacts'; + + Future> loadContacts() async { + final prefs = PrefsManager.instance; + final jsonStr = prefs.getString(_key); + if (jsonStr == null) return []; + + try { + final jsonList = jsonDecode(jsonStr) as List; + return jsonList + .map((entry) => _fromJson(entry as Map)) + .toList(); + } catch (_) { + return []; + } + } + + Future saveContacts(List contacts) async { + final prefs = PrefsManager.instance; + final jsonList = contacts.map(_toJson).toList(); + await prefs.setString(_key, jsonEncode(jsonList)); + } + + Map _toJson(DiscoveryContact contact) { + return { + 'rawPacket': base64Encode(contact.rawPacket), + 'publicKey': base64Encode(contact.publicKey), + 'name': contact.name, + 'type': contact.type, + 'pathLength': contact.pathLength, + 'path': base64Encode(contact.path), + 'latitude': contact.latitude, + 'longitude': contact.longitude, + 'lastSeen': contact.lastSeen.millisecondsSinceEpoch, + }; + } + + DiscoveryContact _fromJson(Map json) { + final lastSeenMs = json['lastSeen'] as int? ?? 0; + return DiscoveryContact( + rawPacket: Uint8List.fromList(base64Decode(json['rawPacket'] as String)), + publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)), + name: json['name'] as String? ?? 'Unknown', + type: json['type'] as int? ?? 0, + pathLength: json['pathLength'] as int? ?? -1, + path: json['path'] != null + ? Uint8List.fromList(base64Decode(json['path'] as String)) + : Uint8List(0), + latitude: (json['latitude'] as num?)?.toDouble(), + longitude: (json['longitude'] as num?)?.toDouble(), + lastSeen: DateTime.fromMillisecondsSinceEpoch(lastSeenMs), + ); + } +} diff --git a/lib/utils/contact_search.dart b/lib/utils/contact_search.dart index 318f307..beec880 100644 --- a/lib/utils/contact_search.dart +++ b/lib/utils/contact_search.dart @@ -1,3 +1,5 @@ +import 'package:meshcore_open/models/discovery_contact.dart'; + import '../models/contact.dart'; bool matchesContactQuery(Contact contact, String query) { @@ -14,6 +16,20 @@ bool matchesContactQuery(Contact contact, String query) { return contact.publicKeyHex.toLowerCase().startsWith(hexPrefix); } +bool matchesDiscoveryContactQuery(DiscoveryContact contact, String query) { + final normalizedQuery = query.trim().toLowerCase(); + if (normalizedQuery.isEmpty) return true; + + if (contact.name.toLowerCase().contains(normalizedQuery)) { + return true; + } + + final hexPrefix = _extractHexPrefix(normalizedQuery); + if (hexPrefix == null) return false; + + return contact.publicKeyHex.toLowerCase().startsWith(hexPrefix); +} + String? _extractHexPrefix(String query) { var cleaned = query; if (cleaned.startsWith('<')) { diff --git a/lib/widgets/app_bar.dart b/lib/widgets/app_bar.dart index 1afc531..b95984b 100644 --- a/lib/widgets/app_bar.dart +++ b/lib/widgets/app_bar.dart @@ -9,7 +9,16 @@ class AppBarTitle extends StatelessWidget { final String title; final Widget? leading; final Widget? trailing; - const AppBarTitle(this.title, {this.leading, this.trailing, super.key}); + final bool indicators; + final bool subtitle; + const AppBarTitle( + this.title, { + this.leading, + this.trailing, + this.indicators = true, + this.subtitle = true, + super.key, + }); @override Widget build(BuildContext context) { @@ -21,12 +30,12 @@ class AppBarTitle extends StatelessWidget { final availableWidth = constraints.hasBoundedWidth ? constraints.maxWidth : MediaQuery.sizeOf(context).width; - final compact = availableWidth < 240; + final compact = availableWidth < 170; final showSubtitle = - !compact && connector.isConnected && selfName != null; + !compact && connector.isConnected && selfName != null && subtitle; final showBattery = availableWidth >= 60; final showSnr = availableWidth >= 110; - final showIndicators = showBattery || showSnr; + final showIndicators = (showBattery || showSnr) && indicators; return Row( mainAxisAlignment: MainAxisAlignment.start, diff --git a/lib/widgets/list_filter_widget.dart b/lib/widgets/list_filter_widget.dart index 473a3df..ee6fcd4 100644 --- a/lib/widgets/list_filter_widget.dart +++ b/lib/widgets/list_filter_widget.dart @@ -224,3 +224,93 @@ class ContactsFilterMenu extends StatelessWidget { ); } } + +class DiscoveryContactsFilterMenu extends StatelessWidget { + final ContactSortOption sortOption; + final ContactTypeFilter typeFilter; + final ValueChanged onSortChanged; + final ValueChanged onTypeFilterChanged; + + const DiscoveryContactsFilterMenu({ + super.key, + required this.sortOption, + required this.typeFilter, + required this.onSortChanged, + required this.onTypeFilterChanged, + }); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return SortFilterMenu( + tooltip: l10n.listFilter_tooltip, + sections: [ + SortFilterMenuSection( + title: l10n.listFilter_sortBy, + options: [ + SortFilterMenuOption( + value: _actionSortLastSeen, + label: l10n.listFilter_heardRecently, + checked: sortOption == ContactSortOption.lastSeen, + ), + SortFilterMenuOption( + value: _actionSortName, + label: l10n.listFilter_az, + checked: sortOption == ContactSortOption.name, + ), + ], + ), + SortFilterMenuSection( + title: l10n.listFilter_filters, + options: [ + SortFilterMenuOption( + value: _actionFilterAll, + label: l10n.listFilter_all, + checked: typeFilter == ContactTypeFilter.all, + ), + SortFilterMenuOption( + value: _actionFilterUsers, + label: l10n.listFilter_users, + checked: typeFilter == ContactTypeFilter.users, + ), + SortFilterMenuOption( + value: _actionFilterRepeaters, + label: l10n.listFilter_repeaters, + checked: typeFilter == ContactTypeFilter.repeaters, + ), + SortFilterMenuOption( + value: _actionFilterRooms, + label: l10n.listFilter_roomServers, + checked: typeFilter == ContactTypeFilter.rooms, + ), + ], + ), + ], + onSelected: (action) { + switch (action) { + case _actionSortName: + onSortChanged(ContactSortOption.name); + break; + case _actionSortLastSeen: + onSortChanged(ContactSortOption.lastSeen); + break; + case _actionFilterAll: + onTypeFilterChanged(ContactTypeFilter.all); + break; + case _actionFilterUsers: + onTypeFilterChanged(ContactTypeFilter.users); + break; + case _actionFilterFavorites: + onTypeFilterChanged(ContactTypeFilter.favorites); + break; + case _actionFilterRepeaters: + onTypeFilterChanged(ContactTypeFilter.repeaters); + break; + case _actionFilterRooms: + onTypeFilterChanged(ContactTypeFilter.rooms); + break; + } + }, + ); + } +} diff --git a/lib/widgets/path_management_dialog.dart b/lib/widgets/path_management_dialog.dart index c2b6d12..384f92b 100644 --- a/lib/widgets/path_management_dialog.dart +++ b/lib/widgets/path_management_dialog.dart @@ -79,6 +79,7 @@ class _PathManagementDialogState extends State<_PathManagementDialog> { title: context.l10n.contacts_repeaterPathTrace, path: Uint8List.fromList(pathBytes), flipPathRound: true, + targetContact: widget.contact, ), ), ), diff --git a/test/screens/usb_flow_test.dart b/test/screens/usb_flow_test.dart index fdcb5ed..12ecdbe 100644 --- a/test/screens/usb_flow_test.dart +++ b/test/screens/usb_flow_test.dart @@ -130,7 +130,7 @@ void main() { connector.fakeActiveUsbPortDisplayLabel = 'COM6 - KD3CGK mesh-utility.org'; connector.notifyListeners(); - await tester.pump(); + await tester.pump(const Duration(milliseconds: 60)); await tester.tap(find.widgetWithText(FilledButton, 'Connect')); await tester.pump(); @@ -155,6 +155,11 @@ void main() { } else { expect(find.widgetWithText(FloatingActionButton, 'USB'), findsNothing); } + + // ScannerScreen.dispose() schedules disconnect work that debounces notify. + // Drain that debounce timer before test teardown. + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pump(const Duration(milliseconds: 60)); }); group('Error Handling', () {