From 4b215ad574d97b7a5e301cffef00211bf6557047 Mon Sep 17 00:00:00 2001 From: ericz Date: Sat, 17 Jan 2026 17:14:39 +0100 Subject: [PATCH 01/11] Disable Map rotation --- lib/screens/map_screen.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 12e4470..a07ba01 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -273,6 +273,9 @@ class _MapScreenState extends State { initialZoom: initialZoom, minZoom: 2.0, maxZoom: 18.0, + interactionOptions: InteractionOptions( + flags: ~InteractiveFlag.rotate + ), onTap: (_, latLng) { if (_isSelectingPoi) { setState(() { From 1f0b7d8d7b1b30bfeac88b700a4372e931c5c5ff Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 18 Jan 2026 01:02:48 -0800 Subject: [PATCH 02/11] added buildSetCustomVarFrame and setCustomVar --- lib/connector/meshcore_connector.dart | 451 +++++++++++++++++--------- lib/connector/meshcore_protocol.dart | 88 +++-- 2 files changed, 356 insertions(+), 183 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 8b32870..96fb229 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -66,8 +66,10 @@ class MeshCoreConnector extends ChangeNotifier { final Map> _conversations = {}; final Map> _channelMessages = {}; final Set _loadedConversationKeys = {}; - final Map> _processedChannelReactions = {}; // channelIndex -> Set of "reactionKey_emoji" - final Map> _processedContactReactions = {}; // contactPubKeyHex -> Set of "reactionKey_emoji" + final Map> _processedChannelReactions = + {}; // channelIndex -> Set of "reactionKey_emoji" + final Map> _processedContactReactions = + {}; // contactPubKeyHex -> Set of "reactionKey_emoji" StreamSubscription>? _scanSubscription; StreamSubscription? _connectionSubscription; @@ -137,7 +139,8 @@ class MeshCoreConnector extends ChangeNotifier { final ContactStore _contactStore = ContactStore(); final UnreadStore _unreadStore = UnreadStore(); final Map _channelSmazEnabled = {}; - bool _lastSentWasCliCommand = false; // Track if last sent message was a CLI command + bool _lastSentWasCliCommand = + false; // Track if last sent message was a CLI command final Map _contactSmazEnabled = {}; final Set _knownContactKeys = {}; final Map _contactLastReadMs = {}; @@ -166,6 +169,7 @@ class MeshCoreConnector extends ChangeNotifier { } return 'Unknown Device'; } + List get scanResults => List.unmodifiable(_scanResults); List get contacts { final selfKey = _selfPublicKey; @@ -176,6 +180,7 @@ class MeshCoreConnector extends ChangeNotifier { _contacts.where((contact) => !listEquals(contact.publicKey, selfKey)), ); } + List get channels => List.unmodifiable(_channels); bool get isConnected => _state == MeshCoreConnectionState.connected; bool get isLoadingContacts => _isLoadingContacts; @@ -196,7 +201,8 @@ class MeshCoreConnector extends ChangeNotifier { int get maxChannels => _maxChannels; bool get isSyncingQueuedMessages => _isSyncingQueuedMessages; bool get isSyncingChannels => _isSyncingChannels; - int get channelSyncProgress => _isSyncingChannels && _totalChannelsToRequest > 0 + int get channelSyncProgress => + _isSyncingChannels && _totalChannelsToRequest > 0 ? ((_nextChannelIndexToRequest / _totalChannelsToRequest) * 100).round() : 0; int? get batteryPercent => _batteryMillivolts == null @@ -377,7 +383,8 @@ class MeshCoreConnector extends ChangeNotifier { } void setActiveContact(String? contactKeyHex) { - if (contactKeyHex != null && !_shouldTrackUnreadForContactKey(contactKeyHex)) { + if (contactKeyHex != null && + !_shouldTrackUnreadForContactKey(contactKeyHex)) { _activeContactKey = null; return; } @@ -429,7 +436,9 @@ class MeshCoreConnector extends ChangeNotifier { /// Load persisted channel messages for a specific channel Future _loadChannelMessages(int channelIndex) async { - final allMessages = await _channelMessageStore.loadChannelMessages(channelIndex); + final allMessages = await _channelMessageStore.loadChannelMessages( + channelIndex, + ); if (allMessages.isNotEmpty) { // Keep only the most recent N messages in memory to bound memory usage final windowedMessages = allMessages.length > _messageWindowSize @@ -446,7 +455,9 @@ class MeshCoreConnector extends ChangeNotifier { int channelIndex, { int count = 50, }) async { - final allMessages = await _channelMessageStore.loadChannelMessages(channelIndex); + final allMessages = await _channelMessageStore.loadChannelMessages( + channelIndex, + ); final currentMessages = _channelMessages[channelIndex] ?? []; if (allMessages.length <= currentMessages.length) { @@ -551,7 +562,9 @@ class MeshCoreConnector extends ChangeNotifier { final contactKey = pubKeyToHex(message.senderKey); final messages = _conversations[contactKey]; if (messages != null) { - final index = messages.indexWhere((m) => m.messageId == message.messageId); + final index = messages.indexWhere( + (m) => m.messageId == message.messageId, + ); if (index != -1) { messages[index] = message; _messageStore.saveMessages(contactKey, messages); @@ -576,7 +589,9 @@ class MeshCoreConnector extends ChangeNotifier { } Contact _applyAutoSelection(Contact contact, PathSelection? selection) { - if (selection == null || selection.useFlood || selection.pathBytes.isEmpty) { + if (selection == null || + selection.useFlood || + selection.pathBytes.isEmpty) { return contact; } @@ -584,7 +599,9 @@ class MeshCoreConnector extends ChangeNotifier { publicKey: contact.publicKey, name: contact.name, type: contact.type, - pathLength: selection.hopCount >= 0 ? selection.hopCount : contact.pathLength, + pathLength: selection.hopCount >= 0 + ? selection.hopCount + : contact.pathLength, path: Uint8List.fromList(selection.pathBytes), latitude: contact.latitude, longitude: contact.longitude, @@ -593,7 +610,9 @@ class MeshCoreConnector extends ChangeNotifier { ); } - Future startScan({Duration timeout = const Duration(seconds: 10)}) async { + Future startScan({ + Duration timeout = const Duration(seconds: 10), + }) async { if (_state == MeshCoreConnectionState.scanning) return; _scanResults.clear(); @@ -714,7 +733,9 @@ class MeshCoreConnector extends ChangeNotifier { if (attempt == 2) rethrow; } } - _notifySubscription = _txCharacteristic!.onValueReceived.listen(_handleFrame); + _notifySubscription = _txCharacteristic!.onValueReceived.listen( + _handleFrame, + ); _setState(MeshCoreConnectionState.connected); @@ -771,8 +792,7 @@ class MeshCoreConnector extends ChangeNotifier { return result; } - bool get _shouldAutoReconnect => - !_manualDisconnect && _lastDeviceId != null; + bool get _shouldAutoReconnect => !_manualDisconnect && _lastDeviceId != null; void _cancelReconnectTimer() { _reconnectTimer?.cancel(); @@ -799,7 +819,8 @@ class MeshCoreConnector extends ChangeNotifier { return; } - final device = _lastDevice ?? + final device = + _lastDevice ?? (_lastDeviceId == null ? null : BluetoothDevice.fromId(_lastDeviceId!)); @@ -945,20 +966,19 @@ class MeshCoreConnector extends ChangeNotifier { void _scheduleSelfInfoRetry() { _selfInfoRetryTimer?.cancel(); - _selfInfoRetryTimer = Timer.periodic( - const Duration(milliseconds: 3500), - (timer) { - if (!isConnected) { - timer.cancel(); - return; - } - if (!_awaitingSelfInfo) { - timer.cancel(); - return; - } - unawaited(sendFrame(buildAppStartFrame())); - }, - ); + _selfInfoRetryTimer = Timer.periodic(const Duration(milliseconds: 3500), ( + timer, + ) { + if (!isConnected) { + timer.cancel(); + return; + } + if (!_awaitingSelfInfo) { + timer.cancel(); + return; + } + unawaited(sendFrame(buildAppStartFrame())); + }); } Future getContacts({int? since, bool preserveExisting = false}) async { @@ -979,10 +999,7 @@ class MeshCoreConnector extends ChangeNotifier { } Future refreshContactsSinceLastmod() async { - await getContacts( - since: _latestContactLastmod(), - preserveExisting: true, - ); + await getContacts(since: _latestContactLastmod(), preserveExisting: true); } Future getContactByKey(Uint8List pubKey) async { @@ -990,18 +1007,20 @@ class MeshCoreConnector extends ChangeNotifier { await sendFrame(buildGetContactByKeyFrame(pubKey)); } - Future sendMessage( - Contact contact, - String text, - ) async { + Future sendMessage(Contact contact, String text) async { if (!isConnected || text.isEmpty) return; // Handle auto-rotation if enabled PathSelection? autoSelection; if (_appSettingsService?.settings.autoRouteRotationEnabled == true) { - autoSelection = _pathHistoryService?.getNextAutoPathSelection(contact.publicKeyHex); + autoSelection = _pathHistoryService?.getNextAutoPathSelection( + contact.publicKeyHex, + ); if (autoSelection != null) { - _pathHistoryService?.recordPathAttempt(contact.publicKeyHex, autoSelection); + _pathHistoryService?.recordPathAttempt( + contact.publicKeyHex, + autoSelection, + ); if (!autoSelection.useFlood && autoSelection.pathBytes.isNotEmpty) { await setContactPath( contact, @@ -1036,12 +1055,7 @@ class MeshCoreConnector extends ChangeNotifier { _addMessage(contact.publicKeyHex, message); notifyListeners(); final outboundText = prepareContactOutboundText(contact, text); - await sendFrame( - buildSendTextMsgFrame( - contact.publicKey, - outboundText, - ), - ); + await sendFrame(buildSendTextMsgFrame(contact.publicKey, outboundText)); } } @@ -1052,13 +1066,15 @@ class MeshCoreConnector extends ChangeNotifier { ) async { if (!isConnected) return; - await sendFrame(buildUpdateContactPathFrame( - contact.publicKey, - customPath, - pathLen, - type: contact.type, - name: contact.name, - )); + await sendFrame( + buildUpdateContactPathFrame( + contact.publicKey, + customPath, + pathLen, + type: contact.type, + name: contact.name, + ), + ); } /// Set path override for a contact (persists across contact refreshes) @@ -1068,16 +1084,27 @@ class MeshCoreConnector extends ChangeNotifier { int? pathLen, Uint8List? pathBytes, }) async { - appLogger.info('setPathOverride called for ${contact.name}: pathLen=$pathLen, bytesLen=${pathBytes?.length ?? 0}', tag: 'Connector'); + appLogger.info( + 'setPathOverride called for ${contact.name}: pathLen=$pathLen, bytesLen=${pathBytes?.length ?? 0}', + tag: 'Connector', + ); // Find contact in list - final index = _contacts.indexWhere((c) => c.publicKeyHex == contact.publicKeyHex); + final index = _contacts.indexWhere( + (c) => c.publicKeyHex == contact.publicKeyHex, + ); if (index == -1) { - appLogger.warn('setPathOverride: Contact not found in list: ${contact.name}', tag: 'Connector'); + appLogger.warn( + 'setPathOverride: Contact not found in list: ${contact.name}', + tag: 'Connector', + ); return; } - appLogger.info('Found contact at index $index. Current override: ${_contacts[index].pathOverride}', tag: 'Connector'); + appLogger.info( + 'Found contact at index $index. Current override: ${_contacts[index].pathOverride}', + tag: 'Connector', + ); // Update contact with new path override _contacts[index] = _contacts[index].copyWith( @@ -1086,7 +1113,10 @@ class MeshCoreConnector extends ChangeNotifier { clearPathOverride: pathLen == null, // Clear if pathLen is null ); - appLogger.info('Updated contact. New override: ${_contacts[index].pathOverride}, bytesLen: ${_contacts[index].pathOverrideBytes?.length}', tag: 'Connector'); + appLogger.info( + 'Updated contact. New override: ${_contacts[index].pathOverride}, bytesLen: ${_contacts[index].pathOverrideBytes?.length}', + tag: 'Connector', + ); // Save to storage await _contactStore.saveContacts(_contacts); @@ -1099,7 +1129,9 @@ class MeshCoreConnector extends ChangeNotifier { appLogger.info('Path sent to device', tag: 'Connector'); } - debugPrint('Set path override for ${contact.name}: pathLen=$pathLen, bytes=${pathBytes?.length ?? 0}'); + debugPrint( + 'Set path override for ${contact.name}: pathLen=$pathLen, bytes=${pathBytes?.length ?? 0}', + ); notifyListeners(); } @@ -1148,7 +1180,9 @@ class MeshCoreConnector extends ChangeNotifier { outboundText, selfKey, ); - final ackHashHex = ackHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); + final ackHashHex = ackHash + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(); final messageBytes = utf8.encode(outboundText).length; _pendingRepeaterAcks[ackHashHex]?.timeout?.cancel(); _pendingRepeaterAcks[ackHashHex] = _RepeaterAckContext( @@ -1206,7 +1240,7 @@ class MeshCoreConnector extends ChangeNotifier { } } - Future sendChannelMessage(Channel channel, String text) async{ + Future sendChannelMessage(Channel channel, String text) async { if (!isConnected || text.isEmpty) return; // Check if this is a reaction - if so, process it immediately instead of adding as a message @@ -1215,9 +1249,14 @@ class MeshCoreConnector extends ChangeNotifier { // Check if we've already processed this reaction _processedChannelReactions.putIfAbsent(channel.index, () => {}); final reactionKey = reactionInfo.reactionKey; - final reactionIdentifier = reactionKey != null ? '${reactionKey}_${reactionInfo.emoji}' : null; + final reactionIdentifier = reactionKey != null + ? '${reactionKey}_${reactionInfo.emoji}' + : null; - if (reactionIdentifier != null && _processedChannelReactions[channel.index]!.contains(reactionIdentifier)) { + if (reactionIdentifier != null && + _processedChannelReactions[channel.index]!.contains( + reactionIdentifier, + )) { // Already processed, don't process again return; } @@ -1242,13 +1281,19 @@ class MeshCoreConnector extends ChangeNotifier { return; } - final message = ChannelMessage.outgoing(text, _selfName ?? 'Me', channel.index); + final message = ChannelMessage.outgoing( + text, + _selfName ?? 'Me', + channel.index, + ); _addChannelMessage(channel.index, message); notifyListeners(); final trimmed = text.trim(); - final isStructuredPayload = trimmed.startsWith('g:') || trimmed.startsWith('m:'); - final outboundText = (isChannelSmazEnabled(channel.index) && !isStructuredPayload) + final isStructuredPayload = + trimmed.startsWith('g:') || trimmed.startsWith('m:'); + final outboundText = + (isChannelSmazEnabled(channel.index) && !isStructuredPayload) ? Smaz.encodeIfSmaller(text) : text; await sendFrame(buildSendChannelTextMsgFrame(channel.index, outboundText)); @@ -1264,9 +1309,7 @@ class MeshCoreConnector extends ChangeNotifier { _conversations.remove(contact.publicKeyHex); _loadedConversationKeys.remove(contact.publicKeyHex); _contactLastReadMs.remove(contact.publicKeyHex); - _unreadStore.saveContactLastRead( - Map.from(_contactLastReadMs), - ); + _unreadStore.saveContactLastRead(Map.from(_contactLastReadMs)); _messageStore.clearMessages(contact.publicKeyHex); notifyListeners(); } @@ -1275,8 +1318,9 @@ class MeshCoreConnector extends ChangeNotifier { if (!isConnected) return; await sendFrame(buildResetPathFrame(contact.publicKey)); - final existingIndex = - _contacts.indexWhere((c) => c.publicKeyHex == contact.publicKeyHex); + final existingIndex = _contacts.indexWhere( + (c) => c.publicKeyHex == contact.publicKeyHex, + ); if (existingIndex >= 0) { final existing = _contacts[existingIndex]; // Use copyWith to preserve pathOverride and pathOverrideBytes @@ -1295,8 +1339,9 @@ class MeshCoreConnector extends ChangeNotifier { Uint8List? pathBytes, int? pathLength, }) { - final existingIndex = - _contacts.indexWhere((c) => c.publicKeyHex == publicKeyHex); + final existingIndex = _contacts.indexWhere( + (c) => c.publicKeyHex == publicKeyHex, + ); if (existingIndex >= 0) { final existing = _contacts[existingIndex]; _contacts[existingIndex] = existing.copyWith( @@ -1344,7 +1389,9 @@ class MeshCoreConnector extends ChangeNotifier { _handleQueueSyncTimeout(); }); - debugPrint('[QueueSync] Requesting next message (retry: $_queueSyncRetries/$_maxQueueSyncRetries)'); + debugPrint( + '[QueueSync] Requesting next message (retry: $_queueSyncRetries/$_maxQueueSyncRetries)', + ); try { await sendFrame(buildSyncNextMessageFrame()); @@ -1358,7 +1405,9 @@ class MeshCoreConnector extends ChangeNotifier { } void _handleQueueSyncTimeout() { - debugPrint('[QueueSync] Timeout waiting for message (retry: $_queueSyncRetries/$_maxQueueSyncRetries)'); + debugPrint( + '[QueueSync] Timeout waiting for message (retry: $_queueSyncRetries/$_maxQueueSyncRetries)', + ); if (_queueSyncRetries < _maxQueueSyncRetries) { // Retry @@ -1389,11 +1438,19 @@ class MeshCoreConnector extends ChangeNotifier { await sendFrame(buildSetAdvertNameFrame(name)); } - Future setNodeLocation({required double lat, required double lon}) async { + Future setNodeLocation({ + required double lat, + required double lon, + }) async { if (!isConnected) return; await sendFrame(buildSetAdvertLatLonFrame(lat, lon)); } + Future setCustomVar(String value) async { + if (!isConnected) return; + await sendFrame(buildSetCustomVarFrame(value)); + } + Future sendSelfAdvert({bool flood = true}) async { if (!isConnected) return; await sendFrame(buildSendSelfAdvertFrame(flood: flood)); @@ -1424,7 +1481,9 @@ class MeshCoreConnector extends ChangeNotifier { _channelSyncRetries = 0; notifyListeners(); - debugPrint('[ChannelSync] Starting sync for $_totalChannelsToRequest channels'); + debugPrint( + '[ChannelSync] Starting sync for $_totalChannelsToRequest channels', + ); // Start sequential sync await _requestNextChannel(); @@ -1456,7 +1515,9 @@ class MeshCoreConnector extends ChangeNotifier { () => _handleChannelSyncTimeout(channelIndex), ); - debugPrint('[ChannelSync] Requesting channel $channelIndex/$_totalChannelsToRequest (retry: $_channelSyncRetries/$_maxChannelSyncRetries)'); + debugPrint( + '[ChannelSync] Requesting channel $channelIndex/$_totalChannelsToRequest (retry: $_channelSyncRetries/$_maxChannelSyncRetries)', + ); try { await sendFrame(buildGetChannelFrame(channelIndex)); @@ -1468,7 +1529,9 @@ class MeshCoreConnector extends ChangeNotifier { } void _handleChannelSyncTimeout(int channelIndex) { - debugPrint('[ChannelSync] Timeout waiting for channel $channelIndex (retry: $_channelSyncRetries/$_maxChannelSyncRetries)'); + debugPrint( + '[ChannelSync] Timeout waiting for channel $channelIndex (retry: $_channelSyncRetries/$_maxChannelSyncRetries)', + ); if (_channelSyncRetries < _maxChannelSyncRetries) { // Retry the same channel @@ -1477,16 +1540,20 @@ class MeshCoreConnector extends ChangeNotifier { unawaited(_requestNextChannel()); } else { // Max retries reached for this channel, restore from cache and move to next - debugPrint('[ChannelSync] Max retries reached for channel $channelIndex, attempting cache restore'); + debugPrint( + '[ChannelSync] Max retries reached for channel $channelIndex, attempting cache restore', + ); // Try to restore this channel from cache try { final cachedChannel = _previousChannelsCache.firstWhere( - (c) => c.index == channelIndex + (c) => c.index == channelIndex, ); if (!cachedChannel.isEmpty) { _channels.add(cachedChannel); - debugPrint('[ChannelSync] Restored channel $channelIndex (${cachedChannel.name}) from cache'); + debugPrint( + '[ChannelSync] Restored channel $channelIndex (${cachedChannel.name}) from cache', + ); } } catch (e) { // No cached channel found, that's okay @@ -1503,7 +1570,9 @@ class MeshCoreConnector extends ChangeNotifier { void _completeChannelSync() { _channelSyncTimeout?.cancel(); - debugPrint('[ChannelSync] Sync complete: received ${_channels.length}/$_totalChannelsToRequest channels'); + debugPrint( + '[ChannelSync] Sync complete: received ${_channels.length}/$_totalChannelsToRequest channels', + ); _cleanupChannelSync(completed: true); @@ -1541,9 +1610,7 @@ class MeshCoreConnector extends ChangeNotifier { // Delete by setting empty name and zero PSK await sendFrame(buildSetChannelFrame(index, '', Uint8List(16))); _channelLastReadMs.remove(index); - _unreadStore.saveChannelLastRead( - Map.from(_channelLastReadMs), - ); + _unreadStore.saveChannelLastRead(Map.from(_channelLastReadMs)); // Refresh channels after deleting await getChannels(); } @@ -1705,8 +1772,12 @@ class MeshCoreConnector extends ChangeNotifier { // Firmware reports MAX_CONTACTS / 2 for v3+ device info. final reportedContacts = frame[2]; final reportedChannels = frame[3]; - final nextMaxContacts = reportedContacts > 0 ? reportedContacts * 2 : _maxContacts; - final nextMaxChannels = reportedChannels > 0 ? reportedChannels : _maxChannels; + final nextMaxContacts = reportedContacts > 0 + ? reportedContacts * 2 + : _maxContacts; + final nextMaxChannels = reportedChannels > 0 + ? reportedChannels + : _maxChannels; final previousMaxChannels = _maxChannels; if (nextMaxContacts != _maxContacts || nextMaxChannels != _maxChannels) { _maxContacts = nextMaxContacts; @@ -1751,7 +1822,9 @@ class MeshCoreConnector extends ChangeNotifier { _currentBwHz = readUint32LE(frame, 5); _currentSf = frame[9]; _currentCr = frame[10]; - debugPrint('Radio settings: freq=$_currentFreqHz bw=$_currentBwHz sf=$_currentSf cr=$_currentCr'); + debugPrint( + 'Radio settings: freq=$_currentFreqHz bw=$_currentBwHz sf=$_currentSf cr=$_currentCr', + ); notifyListeners(); } } @@ -1822,11 +1895,15 @@ class MeshCoreConnector extends ChangeNotifier { if (existingIndex >= 0) { final existing = _contacts[existingIndex]; - final mergedLastMessageAt = existing.lastMessageAt.isAfter(contact.lastMessageAt) + final mergedLastMessageAt = + existing.lastMessageAt.isAfter(contact.lastMessageAt) ? existing.lastMessageAt : contact.lastMessageAt; - appLogger.info('Refreshing contact ${contact.name}: devicePath=${contact.pathLength}, existingOverride=${existing.pathOverride}', tag: 'Connector'); + appLogger.info( + 'Refreshing contact ${contact.name}: devicePath=${contact.pathLength}, existingOverride=${existing.pathOverride}', + tag: 'Connector', + ); // CRITICAL: Preserve user's path override when contact is refreshed from device _contacts[existingIndex] = contact.copyWith( @@ -1835,10 +1912,16 @@ class MeshCoreConnector extends ChangeNotifier { pathOverrideBytes: existing.pathOverrideBytes, ); - appLogger.info('After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}', tag: 'Connector'); + appLogger.info( + 'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}', + tag: 'Connector', + ); } else { _contacts.add(contact); - appLogger.info('Added new contact ${contact.name}: pathLen=${contact.pathLength}', tag: 'Connector'); + appLogger.info( + 'Added new contact ${contact.name}: pathLen=${contact.pathLength}', + tag: 'Connector', + ); } _knownContactKeys.add(contact.publicKeyHex); _loadMessagesForContact(contact.publicKeyHex); @@ -1970,9 +2053,13 @@ class MeshCoreConnector extends ChangeNotifier { if (message == null && !_isLoadingContacts) { final senderPrefix = _extractSenderPrefix(frame); if (senderPrefix != null) { - final hasContact = _contacts.any((c) => _matchesPrefix(c.publicKey, senderPrefix)); + final hasContact = _contacts.any( + (c) => _matchesPrefix(c.publicKey, senderPrefix), + ); if (!hasContact) { - debugPrint('Received message from unknown contact, refreshing contacts...'); + debugPrint( + 'Received message from unknown contact, refreshing contacts...', + ); await refreshContactsSinceLastmod(); // Retry parsing after refresh message = _parseContactMessage(frame); @@ -2017,7 +2104,9 @@ class MeshCoreConnector extends ChangeNotifier { notifyListeners(); // Show notification for new incoming message - if (!message.isOutgoing && !message.isCli && _appSettingsService != null) { + if (!message.isOutgoing && + !message.isCli && + _appSettingsService != null) { final settings = _appSettingsService!.settings; if (settings.notificationsEnabled && settings.notifyOnNewMessage) { // Find the contact name @@ -2074,9 +2163,17 @@ class MeshCoreConnector extends ChangeNotifier { // Try base text offset; if empty and there is room for the optional 4-byte extra // (used by signed/plain variants), try again skipping those bytes. - var text = readCString(frame, baseTextOffset, frame.length - baseTextOffset); + var text = readCString( + frame, + baseTextOffset, + frame.length - baseTextOffset, + ); if (text.isEmpty && frame.length > baseTextOffset + 4) { - text = readCString(frame, baseTextOffset + 4, frame.length - (baseTextOffset + 4)); + text = readCString( + frame, + baseTextOffset + 4, + frame.length - (baseTextOffset + 4), + ); } if (text.isEmpty) return null; final decodedText = isCli ? text : (Smaz.tryDecodePrefixed(text) ?? text); @@ -2099,7 +2196,7 @@ class MeshCoreConnector extends ChangeNotifier { status: MessageStatus.delivered, pathLength: pathLenByte == 0xFF ? 0 : pathLenByte, pathBytes: Uint8List(0), - fourByteRoomContactKey: fourBytePubMSG + fourByteRoomContactKey: fourBytePubMSG, ); } @@ -2140,17 +2237,15 @@ class MeshCoreConnector extends ChangeNotifier { String prepareContactOutboundText(Contact contact, String text) { final trimmed = text.trim(); final isStructuredPayload = - trimmed.startsWith('g:') || trimmed.startsWith('m:') || trimmed.startsWith('V1|'); + trimmed.startsWith('g:') || + trimmed.startsWith('m:') || + trimmed.startsWith('V1|'); if (!isStructuredPayload && isContactSmazEnabled(contact.publicKeyHex)) { return Smaz.encodeIfSmaller(text); } return text; } - - - - String _channelDisplayName(int channelIndex) { for (final channel in _channels) { if (channel.index != channelIndex) continue; @@ -2184,7 +2279,10 @@ class MeshCoreConnector extends ChangeNotifier { void _handleIncomingChannelMessage(Uint8List frame) { final message = ChannelMessage.fromFrame(frame); if (message != null && message.channelIndex != null) { - if (_shouldDropSelfChannelMessage(message.senderName, message.pathBytes)) { + if (_shouldDropSelfChannelMessage( + message.senderName, + message.pathBytes, + )) { return; } _updateContactLastMessageAtByName( @@ -2257,7 +2355,9 @@ class MeshCoreConnector extends ChangeNotifier { _maybeMarkActiveChannelRead(message); notifyListeners(); if (isNew) { - final label = channel.name.isEmpty ? 'Channel ${channel.index}' : channel.name; + final label = channel.name.isEmpty + ? 'Channel ${channel.index}' + : channel.name; _maybeNotifyChannelMessage(message, channelName: label); } return; @@ -2277,7 +2377,9 @@ class MeshCoreConnector extends ChangeNotifier { // Check if this is a CLI command ACK - if so, ignore it if (_lastSentWasCliCommand) { - final ackHashHex = ackHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); + final ackHashHex = ackHash + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(); debugPrint('Ignoring CLI command ACK (sent): $ackHashHex'); _lastSentWasCliCommand = false; return; @@ -2294,7 +2396,8 @@ class MeshCoreConnector extends ChangeNotifier { // Fallback to old behavior for (var messages in _conversations.values) { for (int i = messages.length - 1; i >= 0; i--) { - if (messages[i].isOutgoing && messages[i].status == MessageStatus.pending) { + if (messages[i].isOutgoing && + messages[i].status == MessageStatus.pending) { messages[i] = messages[i].copyWith(status: MessageStatus.sent); notifyListeners(); return; @@ -2328,7 +2431,8 @@ class MeshCoreConnector extends ChangeNotifier { // Fallback to old behavior for (var messages in _conversations.values) { for (int i = messages.length - 1; i >= 0; i--) { - if (messages[i].isOutgoing && messages[i].status == MessageStatus.sent) { + if (messages[i].isOutgoing && + messages[i].status == MessageStatus.sent) { messages[i] = messages[i].copyWith(status: MessageStatus.delivered); notifyListeners(); return; @@ -2339,7 +2443,9 @@ class MeshCoreConnector extends ChangeNotifier { } bool _handleRepeaterCommandSent(Uint8List ackHash, int timeoutMs) { - final ackHashHex = ackHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); + final ackHashHex = ackHash + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(); final entry = _pendingRepeaterAcks[ackHashHex]; if (entry == null) return false; @@ -2358,7 +2464,9 @@ class MeshCoreConnector extends ChangeNotifier { } bool _handleRepeaterCommandAck(Uint8List ackHash, int tripTimeMs) { - final ackHashHex = ackHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); + final ackHashHex = ackHash + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(); final entry = _pendingRepeaterAcks.remove(ackHashHex); if (entry == null) return false; entry.timeout?.cancel(); @@ -2370,7 +2478,9 @@ class MeshCoreConnector extends ChangeNotifier { final channel = Channel.fromFrame(frame); if (channel == null) return; - debugPrint('[ChannelSync] Received channel ${channel.index}: ${channel.isEmpty ? "empty" : channel.name}'); + debugPrint( + '[ChannelSync] Received channel ${channel.index}: ${channel.isEmpty ? "empty" : channel.name}', + ); // If we're syncing and this is the channel we're waiting for if (_isSyncingChannels && _channelSyncInFlight) { @@ -2392,9 +2502,12 @@ class MeshCoreConnector extends ChangeNotifier { } else { // Received a channel but not the one we're waiting for // This can happen if device sends unsolicited updates - debugPrint('[ChannelSync] Received unexpected channel ${channel.index}, expected $_nextChannelIndexToRequest'); + debugPrint( + '[ChannelSync] Received unexpected channel ${channel.index}, expected $_nextChannelIndexToRequest', + ); // Add it anyway but don't advance sync - if (!channel.isEmpty && !_channels.any((c) => c.index == channel.index)) { + if (!channel.isEmpty && + !_channels.any((c) => c.index == channel.index)) { _channels.add(channel); } return; @@ -2404,7 +2517,9 @@ class MeshCoreConnector extends ChangeNotifier { // Not syncing, or received unsolicited update - handle normally if (!channel.isEmpty) { // Update or add channel - final existingIndex = _channels.indexWhere((c) => c.index == channel.index); + final existingIndex = _channels.indexWhere( + (c) => c.index == channel.index, + ); if (existingIndex >= 0) { _channels[existingIndex] = channel; } else { @@ -2469,26 +2584,30 @@ class MeshCoreConnector extends ChangeNotifier { return latestMs; } - void _setContactLastReadMs(String contactKeyHex, int timestampMs, {bool notify = true}) { + void _setContactLastReadMs( + String contactKeyHex, + int timestampMs, { + bool notify = true, + }) { if (!_shouldTrackUnreadForContactKey(contactKeyHex)) return; final existing = _contactLastReadMs[contactKeyHex] ?? 0; if (timestampMs <= existing) return; _contactLastReadMs[contactKeyHex] = timestampMs; - _unreadStore.saveContactLastRead( - Map.from(_contactLastReadMs), - ); + _unreadStore.saveContactLastRead(Map.from(_contactLastReadMs)); if (notify) { notifyListeners(); } } - void _setChannelLastReadMs(int channelIndex, int timestampMs, {bool notify = true}) { + void _setChannelLastReadMs( + int channelIndex, + int timestampMs, { + bool notify = true, + }) { final existing = _channelLastReadMs[channelIndex] ?? 0; if (timestampMs <= existing) return; _channelLastReadMs[channelIndex] = timestampMs; - _unreadStore.saveChannelLastRead( - Map.from(_channelLastReadMs), - ); + _unreadStore.saveChannelLastRead(Map.from(_channelLastReadMs)); if (notify) { notifyListeners(); } @@ -2526,9 +2645,12 @@ class MeshCoreConnector extends ChangeNotifier { // Check if we've already processed this exact reaction using lightweight key _processedContactReactions.putIfAbsent(pubKeyHex, () => {}); final reactionKey = reactionInfo.reactionKey; - final reactionIdentifier = reactionKey != null ? '${reactionKey}_${reactionInfo.emoji}' : null; + final reactionIdentifier = reactionKey != null + ? '${reactionKey}_${reactionInfo.emoji}' + : null; - final isDuplicate = reactionIdentifier != null && + final isDuplicate = + reactionIdentifier != null && _processedContactReactions[pubKeyHex]!.contains(reactionIdentifier); if (!isDuplicate) { @@ -2551,7 +2673,10 @@ class MeshCoreConnector extends ChangeNotifier { notifyListeners(); } - void _processContactReaction(List messages, ReactionInfo reactionInfo) { + void _processContactReaction( + List messages, + ReactionInfo reactionInfo, + ) { // Find target message by messageId for (int i = 0; i < messages.length; i++) { if (messages[i].messageId == reactionInfo.targetMessageId) { @@ -2570,7 +2695,8 @@ class MeshCoreConnector extends ChangeNotifier { var index = 0; final header = raw[index++]; final routeType = header & _phRouteMask; - final hasTransport = routeType == _routeTransportFlood || routeType == _routeTransportDirect; + final hasTransport = + routeType == _routeTransportFlood || routeType == _routeTransportDirect; if (hasTransport) { if (raw.length < index + 4) return null; index += 4; @@ -2633,7 +2759,8 @@ class MeshCoreConnector extends ChangeNotifier { if (RegExp(r'[:\[\]]').hasMatch(potentialSender)) { return _ParsedText(senderName: 'Unknown', text: text); } - final offset = (colonIndex + 1 < text.length && text[colonIndex + 1] == ' ') + final offset = + (colonIndex + 1 < text.length && text[colonIndex + 1] == ' ') ? colonIndex + 2 : colonIndex + 1; return _ParsedText( @@ -2670,10 +2797,7 @@ class MeshCoreConnector extends ChangeNotifier { return contact.path; } - int? _resolveOutgoingPathLength( - Contact contact, - PathSelection? selection, - ) { + int? _resolveOutgoingPathLength(Contact contact, PathSelection? selection) { // Priority 1: Check user's path override if (contact.pathOverride != null) { return contact.pathOverride; @@ -2714,10 +2838,15 @@ class MeshCoreConnector extends ChangeNotifier { // Check if we've already processed this exact reaction using lightweight key _processedChannelReactions.putIfAbsent(channelIndex, () => {}); final reactionKey = reactionInfo.reactionKey; - final reactionIdentifier = reactionKey != null ? '${reactionKey}_${reactionInfo.emoji}' : null; + final reactionIdentifier = reactionKey != null + ? '${reactionKey}_${reactionInfo.emoji}' + : null; - final isDuplicate = reactionIdentifier != null && - _processedChannelReactions[channelIndex]!.contains(reactionIdentifier); + final isDuplicate = + reactionIdentifier != null && + _processedChannelReactions[channelIndex]!.contains( + reactionIdentifier, + ); if (!isDuplicate) { // New reaction - process it @@ -2739,7 +2868,10 @@ class MeshCoreConnector extends ChangeNotifier { if (replyInfo != null) { // Find original message by sender name (most recent match) - final originalMessage = _findMessageBySender(messages, replyInfo.mentionedNode); + final originalMessage = _findMessageBySender( + messages, + replyInfo.mentionedNode, + ); if (originalMessage != null) { // Create new message with reply metadata @@ -2769,8 +2901,14 @@ class MeshCoreConnector extends ChangeNotifier { if (existingIndex >= 0) { isNew = false; final existing = messages[existingIndex]; - final mergedPathBytes = _selectPreferredPathBytes(existing.pathBytes, processedMessage.pathBytes); - final mergedPathVariants = _mergePathVariants(existing.pathVariants, processedMessage.pathVariants); + final mergedPathBytes = _selectPreferredPathBytes( + existing.pathBytes, + processedMessage.pathBytes, + ); + final mergedPathVariants = _mergePathVariants( + existing.pathVariants, + processedMessage.pathVariants, + ); final mergedPathLength = _mergePathLength( existing.pathLength, processedMessage.pathLength, @@ -2783,7 +2921,9 @@ class MeshCoreConnector extends ChangeNotifier { pathBytes: mergedPathBytes, pathVariants: mergedPathVariants, // Mark as sent when first repeat is heard - status: newRepeatCount == 1 && existing.status == ChannelMessageStatus.pending + status: + newRepeatCount == 1 && + existing.status == ChannelMessageStatus.pending ? ChannelMessageStatus.sent : existing.status, ); @@ -2792,14 +2932,14 @@ class MeshCoreConnector extends ChangeNotifier { } // Save to persistent storage - _channelMessageStore.saveChannelMessages( - channelIndex, - messages, - ); + _channelMessageStore.saveChannelMessages(channelIndex, messages); return isNew; } - ChannelMessage? _findMessageBySender(List messages, String mentionedNode) { + ChannelMessage? _findMessageBySender( + List messages, + String mentionedNode, + ) { // Search backwards for most recent message from this sender for (int i = messages.length - 1; i >= 0; i--) { if (messages[i].senderName == mentionedNode && !messages[i].isOutgoing) { @@ -2809,7 +2949,10 @@ class MeshCoreConnector extends ChangeNotifier { return null; } - void _processReaction(List messages, ReactionInfo reactionInfo) { + void _processReaction( + List messages, + ReactionInfo reactionInfo, + ) { // Find target message by messageId for (int i = 0; i < messages.length; i++) { if (messages[i].messageId == reactionInfo.targetMessageId) { @@ -2824,7 +2967,10 @@ class MeshCoreConnector extends ChangeNotifier { } } - int _findChannelRepeatIndex(List messages, ChannelMessage incoming) { + int _findChannelRepeatIndex( + List messages, + ChannelMessage incoming, + ) { for (int i = messages.length - 1; i >= 0; i--) { final existing = messages[i]; if (_isChannelRepeat(existing, incoming)) { @@ -2837,9 +2983,10 @@ class MeshCoreConnector extends ChangeNotifier { bool _isChannelRepeat(ChannelMessage existing, ChannelMessage incoming) { if (existing.text != incoming.text) return false; - final diffMs = (existing.timestamp.millisecondsSinceEpoch - - incoming.timestamp.millisecondsSinceEpoch) - .abs(); + final diffMs = + (existing.timestamp.millisecondsSinceEpoch - + incoming.timestamp.millisecondsSinceEpoch) + .abs(); if (diffMs > 5000) return false; if (existing.senderName == incoming.senderName) return true; @@ -3013,17 +3160,15 @@ class _RawPacket { required this.payload, }); - bool get isFlood => routeType == _routeFlood || routeType == _routeTransportFlood; + bool get isFlood => + routeType == _routeFlood || routeType == _routeTransportFlood; } class _ParsedText { final String senderName; final String text; - _ParsedText({ - required this.senderName, - required this.text, - }); + _ParsedText({required this.senderName, required this.text}); } class _RepeaterAckContext { diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index c828a1a..21c4392 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -20,7 +20,8 @@ class BufferReader { Uint8List readRemainingBytes() => readBytes(remaining); - String readString() => utf8.decode(readRemainingBytes(), allowMalformed: true); + String readString() => + utf8.decode(readRemainingBytes(), allowMalformed: true); String readCString(int maxLength) { final value = []; @@ -38,13 +39,19 @@ class BufferReader { int readUInt8() => readBytes(1).buffer.asByteData().getUint8(0); int readInt8() => readBytes(1).buffer.asByteData().getInt8(0); - int readUInt16LE() => readBytes(2).buffer.asByteData().getUint16(0, Endian.little); - int readUInt16BE() => readBytes(2).buffer.asByteData().getUint16(0, Endian.big); - int readUInt32LE() => readBytes(4).buffer.asByteData().getUint32(0, Endian.little); - int readUInt32BE() => readBytes(4).buffer.asByteData().getUint32(0, Endian.big); - int readInt16LE() => readBytes(2).buffer.asByteData().getInt16(0, Endian.little); + int readUInt16LE() => + readBytes(2).buffer.asByteData().getUint16(0, Endian.little); + int readUInt16BE() => + readBytes(2).buffer.asByteData().getUint16(0, Endian.big); + int readUInt32LE() => + readBytes(4).buffer.asByteData().getUint32(0, Endian.little); + int readUInt32BE() => + readBytes(4).buffer.asByteData().getUint32(0, Endian.big); + int readInt16LE() => + readBytes(2).buffer.asByteData().getInt16(0, Endian.little); int readInt16BE() => readBytes(2).buffer.asByteData().getInt16(0, Endian.big); - int readInt32LE() => readBytes(4).buffer.asByteData().getInt32(0, Endian.little); + int readInt32LE() => + readBytes(4).buffer.asByteData().getInt32(0, Endian.little); int readInt24BE() { var value = (readByte() << 16) | (readByte() << 8) | readByte(); @@ -63,21 +70,25 @@ class BufferWriter { void writeBytes(Uint8List bytes) => _builder.add(bytes); void writeUInt16LE(int num) { - final bytes = Uint8List(2)..buffer.asByteData().setUint16(0, num, Endian.little); + final bytes = Uint8List(2) + ..buffer.asByteData().setUint16(0, num, Endian.little); writeBytes(bytes); } void writeUInt32LE(int num) { - final bytes = Uint8List(4)..buffer.asByteData().setUint32(0, num, Endian.little); + final bytes = Uint8List(4) + ..buffer.asByteData().setUint32(0, num, Endian.little); writeBytes(bytes); } void writeInt32LE(int num) { - final bytes = Uint8List(4)..buffer.asByteData().setInt32(0, num, Endian.little); + final bytes = Uint8List(4) + ..buffer.asByteData().setInt32(0, num, Endian.little); writeBytes(bytes); } - void writeString(String string) => writeBytes(Uint8List.fromList(utf8.encode(string))); + void writeString(String string) => + writeBytes(Uint8List.fromList(utf8.encode(string))); void writeCString(String string, int maxLength) { final bytes = Uint8List(maxLength); @@ -118,6 +129,7 @@ const int cmdGetChannel = 31; const int cmdSetChannel = 32; const int cmdGetRadioSettings = 57; const int cmdGetTelemetryReq = 39; +const int cmdSetCustomVar = 41; const int cmdSendBinaryReq = 50; // Text message types @@ -166,7 +178,6 @@ const int pushCodeNewAdvert = 0x8A; const int pushCodeTelemetryResponse = 0x8B; const int pushCodeBinaryResponse = 0x8C; - // Contact/advertisement types const int advTypeChat = 1; const int advTypeRepeater = 2; @@ -233,10 +244,7 @@ class ParsedContactText { final Uint8List senderPrefix; final String text; - const ParsedContactText({ - required this.senderPrefix, - required this.text, - }); + const ParsedContactText({required this.senderPrefix, required this.text}); } ParsedContactText? parseContactMessageText(Uint8List frame) { @@ -265,10 +273,17 @@ ParsedContactText? parseContactMessageText(Uint8List frame) { return null; } - var text = readCString(frame, baseTextOffset, frame.length - baseTextOffset).trim(); + var text = readCString( + frame, + baseTextOffset, + frame.length - baseTextOffset, + ).trim(); if (text.isEmpty && frame.length > baseTextOffset + 4) { - text = - readCString(frame, baseTextOffset + 4, frame.length - (baseTextOffset + 4)).trim(); + text = readCString( + frame, + baseTextOffset + 4, + frame.length - (baseTextOffset + 4), + ).trim(); } if (text.isEmpty) return null; @@ -362,7 +377,8 @@ Uint8List buildSendTextMsgFrame( int attempt = 0, int? timestampSeconds, }) { - final timestamp = timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000); + final timestamp = + timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000); final writer = BufferWriter(); writer.writeByte(cmdSendTxtMsg); writer.writeByte(txtTypePlain); @@ -444,7 +460,9 @@ Uint8List buildSendSelfAdvertFrame({bool flood = false}) { // Format: [cmd][name...] Uint8List buildSetAdvertNameFrame(String name) { final nameBytes = utf8.encode(name); - final nameLen = nameBytes.length < maxNameSize ? nameBytes.length : maxNameSize - 1; + final nameLen = nameBytes.length < maxNameSize + ? nameBytes.length + : maxNameSize - 1; final writer = BufferWriter(); writer.writeByte(cmdSetAdvertName); writer.writeBytes(Uint8List.fromList(nameBytes.sublist(0, nameLen))); @@ -461,6 +479,14 @@ Uint8List buildSetAdvertLatLonFrame(double lat, double lon) { return writer.toBytes(); } +Uint8List buildSetCustomVarFrame(String value) { + final writer = BufferWriter(); + writer.writeByte(cmdSetCustomVar); + writer.writeString(value); + writer.writeByte(0); + return writer.toBytes(); +} + // Build CMD_REBOOT frame // Format: [cmd]["reboot"] Uint8List buildRebootFrame() { @@ -544,7 +570,9 @@ Uint8List buildUpdateContactPathFrame( // Path data (64 bytes, zero-padded) final pathPadded = Uint8List(maxPathSize); if (customPath.isNotEmpty && pathLen > 0) { - final copyLen = customPath.length < maxPathSize ? customPath.length : maxPathSize; + final copyLen = customPath.length < maxPathSize + ? customPath.length + : maxPathSize; for (int i = 0; i < copyLen; i++) { pathPadded[i] = customPath[i]; } @@ -598,9 +626,11 @@ int calculateLoRaAirtime({ final crc = 1; // CRC enabled final de = lowDataRateOptimize ? 1 : 0; - final numerator = 8 * payloadBytes - 4 * spreadingFactor + 28 + 16 * crc - headerBytes; + final numerator = + 8 * payloadBytes - 4 * spreadingFactor + 28 + 16 * crc - headerBytes; final denominator = 4 * (spreadingFactor - 2 * de); - var payloadSymbols = 8 + ((numerator / denominator).ceil()) * (codingRate + 4); + var payloadSymbols = + 8 + ((numerator / denominator).ceil()) * (codingRate + 4); if (payloadSymbols < 0) { payloadSymbols = 8; @@ -647,7 +677,8 @@ Uint8List buildSendCliCommandFrame( int attempt = 0, int? timestampSeconds, }) { - final timestamp = timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000); + final timestamp = + timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000); final writer = BufferWriter(); writer.writeByte(cmdSendTxtMsg); writer.writeByte(txtTypeCliData); @@ -661,10 +692,7 @@ Uint8List buildSendCliCommandFrame( // Build a telemetry request frame // Format: [cmd][pub_key x32][payload] -Uint8List buildSendBinaryReq( - Uint8List repeaterPubKey, { - Uint8List? payload, -}) { +Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) { final writer = BufferWriter(); writer.writeByte(cmdSendBinaryReq); writer.writeBytes(repeaterPubKey); @@ -672,4 +700,4 @@ Uint8List buildSendBinaryReq( writer.writeBytes(payload); } return writer.toBytes(); -} \ No newline at end of file +} From 2e1a5e0fbf3b7c51446a219ffb3b61ba7100687d Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 18 Jan 2026 01:03:45 -0800 Subject: [PATCH 03/11] added CMD_SET_CUSTOM_VAR to BLE debug --- lib/services/ble_debug_log_service.dart | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/lib/services/ble_debug_log_service.dart b/lib/services/ble_debug_log_service.dart index 002161b..a53ad5d 100644 --- a/lib/services/ble_debug_log_service.dart +++ b/lib/services/ble_debug_log_service.dart @@ -16,7 +16,9 @@ class BleDebugLogEntry { String get hexPreview { const maxBytes = 64; - final bytes = payload.length > maxBytes ? payload.sublist(0, maxBytes) : payload; + final bytes = payload.length > maxBytes + ? payload.sublist(0, maxBytes) + : payload; final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' '); return payload.length > maxBytes ? '$hex …' : hex; } @@ -26,14 +28,13 @@ class BleRawLogRxEntry { final DateTime timestamp; final Uint8List payload; - BleRawLogRxEntry({ - required this.timestamp, - required this.payload, - }); + BleRawLogRxEntry({required this.timestamp, required this.payload}); String get hexPreview { const maxBytes = 64; - final bytes = payload.length > maxBytes ? payload.sublist(0, maxBytes) : payload; + final bytes = payload.length > maxBytes + ? payload.sublist(0, maxBytes) + : payload; final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' '); return payload.length > maxBytes ? '$hex …' : hex; } @@ -45,7 +46,8 @@ class BleDebugLogService extends ChangeNotifier { final List _rawLogRxEntries = []; List get entries => List.unmodifiable(_entries); - List get rawLogRxEntries => List.unmodifiable(_rawLogRxEntries); + List get rawLogRxEntries => + List.unmodifiable(_rawLogRxEntries); void logFrame(Uint8List frame, {required bool outgoing, String? note}) { if (frame.isEmpty) return; @@ -85,7 +87,12 @@ class BleDebugLogService extends ChangeNotifier { notifyListeners(); } - String _describeFrame(int code, Uint8List frame, bool outgoing, String? note) { + String _describeFrame( + int code, + Uint8List frame, + bool outgoing, + String? note, + ) { final label = _codeLabel(code, outgoing: outgoing); final prefix = outgoing ? 'TX' : 'RX'; final extra = _frameDetail(code, frame); @@ -147,6 +154,8 @@ class BleDebugLogService extends ChangeNotifier { return 'CMD_SET_CHANNEL'; case cmdGetRadioSettings: return 'CMD_GET_RADIO_SETTINGS'; + case cmdSetCustomVar: + return 'CMD_SET_CUSTOM_VAR'; default: return null; } From 714aecd7e6685ee5ddc4e44d859c51a87fe4272c Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 18 Jan 2026 01:05:46 -0800 Subject: [PATCH 04/11] Added GPS enable and interval settings --- lib/l10n/app_bg.arb | 7 +- lib/l10n/app_de.arb | 7 +- lib/l10n/app_en.arb | 5 + lib/l10n/app_es.arb | 7 +- lib/l10n/app_fr.arb | 7 +- lib/l10n/app_it.arb | 7 +- lib/l10n/app_localizations.dart | 26 ++- lib/l10n/app_localizations_bg.dart | 14 ++ lib/l10n/app_localizations_de.dart | 14 ++ lib/l10n/app_localizations_en.dart | 16 +- lib/l10n/app_localizations_es.dart | 14 ++ lib/l10n/app_localizations_fr.dart | 14 ++ lib/l10n/app_localizations_it.dart | 14 ++ lib/l10n/app_localizations_nl.dart | 14 ++ lib/l10n/app_localizations_pl.dart | 14 ++ lib/l10n/app_localizations_pt.dart | 14 ++ lib/l10n/app_localizations_sk.dart | 14 ++ lib/l10n/app_localizations_sl.dart | 14 ++ lib/l10n/app_localizations_sv.dart | 14 ++ lib/l10n/app_localizations_zh.dart | 12 + lib/l10n/app_nl.arb | 7 +- lib/l10n/app_pl.arb | 7 +- lib/l10n/app_pt.arb | 7 +- lib/l10n/app_sk.arb | 7 +- lib/l10n/app_sl.arb | 7 +- lib/l10n/app_sv.arb | 7 +- lib/l10n/app_zh.arb | 7 +- lib/screens/settings_screen.dart | 346 +++++++++++++++++++---------- 28 files changed, 495 insertions(+), 148 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 30a2c1b..effbd6c 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Входът не беше успешен. Или паролата е грешна, или повторителят е недостъпен.", - "common_reload": "Презареди", "common_clear": "Изчисти", "path_currentPath": "Текущ път: {path}", @@ -1349,5 +1348,9 @@ "channels_scanQrCode": "Сканирайте QR код", "channels_scanQrCodeComingSoon": "Ще излезе скоро", "channels_enterHashtag": "Въведете хаштаг", - "channels_hashtagHint": "напр. #отбор" + "channels_hashtagHint": "напр. #отбор", + "settings_locationIntervalSec": "Интервал (Секунди)", + "settings_locationGPSEnableSubtitle": "Активирайте GPS, за автоматично изпращане на данни за местоположението (ако е поддържано).", + "settings_locationIntervalInvalid": "Интервалът трябва да бъде поне 60 секунди и по-малко от 86400 секунди.", + "settings_locationGPSEnable": "Активиране на GPS" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 9d2701f..dead738 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Anmeldung fehlgeschlagen. Entweder ist das Passwort falsch oder der Repeater ist nicht erreichbar.", - "common_reload": "Neu laden", "common_clear": "Löschen", "path_currentPath": "Aktiver Pfad: {path}", @@ -1349,5 +1348,9 @@ "channels_scanQrCode": "Scannen Sie einen QR-Code", "channels_scanQrCodeComingSoon": "Bald verfügbar", "channels_enterHashtag": "Gib Hashtag ein", - "channels_hashtagHint": "z.B. #team" + "channels_hashtagHint": "z.B. #team", + "settings_locationGPSEnable": "GPS aktivieren", + "settings_locationGPSEnableSubtitle": "Aktivieren Sie GPS, um Standortdaten automatisch zu senden (falls unterstützt).", + "settings_locationIntervalSec": "Zeitintervall (Sekunden)", + "settings_locationIntervalInvalid": "Der Zeitraum muss mindestens 60 Sekunden betragen und weniger als 86400 Sekunden sein." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 203f64f..9b2547c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -86,6 +86,11 @@ "settings_locationUpdated": "Location updated", "settings_locationBothRequired": "Enter both latitude and longitude.", "settings_locationInvalid": "Invalid latitude or longitude.", + "settings_locationGPSEnable": "GPS Enable", + "settings_locationGPSEnableSubtitle": "Enable GPS to automatically send location data (if supported)", + "settings_locationIntervalSec": "Interval (Seconds)", + "settings_locationIntervalInvalid": "Interval must be at least 60 seconds, and less than 86400 seconds.", + "settings_locationUpdated": "GPS settings updated.", "settings_latitude": "Latitude", "settings_longitude": "Longitude", "settings_privacyMode": "Privacy Mode", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index b23cfce..bf90fe4 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Inicio fallido. La contraseña es incorrecta o el repetidor no está disponible.", - "common_reload": "Recargar", "common_clear": "Borrar", "path_currentPath": "Ruta actual: {path}", @@ -1349,5 +1348,9 @@ "channels_scanQrCode": "Escanear un Código QR", "channels_scanQrCodeComingSoon": "Próximamente", "channels_enterHashtag": "Introducir hashtag", - "channels_hashtagHint": "ej. #equipo" + "channels_hashtagHint": "ej. #equipo", + "settings_locationGPSEnableSubtitle": "Habilitar GPS para enviar automáticamente datos de ubicación (si está disponible).", + "settings_locationGPSEnable": "Habilitar GPS", + "settings_locationIntervalSec": "Intervalo (Segundos)", + "settings_locationIntervalInvalid": "El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 0e7b952..82d73d7 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Connexion échouée. Soit le mot de passe est incorrect, soit le relais est injoignable.", - "common_reload": "Recharger", "common_clear": "Effacer", "path_currentPath": "Chemin actuel : {path}", @@ -1349,5 +1348,9 @@ "channels_scanQrCode": "Scanner un code QR", "channels_scanQrCodeComingSoon": "Bientôt disponible", "channels_enterHashtag": "Entrez le hashtag", - "channels_hashtagHint": "ex. #équipe" + "channels_hashtagHint": "ex. #équipe", + "settings_locationGPSEnable": "Activer le GPS", + "settings_locationGPSEnableSubtitle": "Activer le GPS pour envoyer automatiquement les données de localisation (si pris en charge).", + "settings_locationIntervalSec": "Intervalle (Secondes)", + "settings_locationIntervalInvalid": "L'intervalle doit être d'au moins 60 secondes et inférieur à 86400 secondes." } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index b77450f..f9828e5 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.", - "common_reload": "Ricaricare", "common_clear": "Cancella", "path_currentPath": "Percorso corrente: {path}", @@ -1349,5 +1348,9 @@ "channels_scanQrCode": "Scansiona un codice QR", "channels_scanQrCodeComingSoon": "Arriverà presto", "channels_enterHashtag": "Inserisci hashtag", - "channels_hashtagHint": "es. #team" + "channels_hashtagHint": "es. #team", + "settings_locationGPSEnableSubtitle": "Abilita il GPS per inviare automaticamente i dati di posizione (se supportato).", + "settings_locationGPSEnable": "Abilita GPS", + "settings_locationIntervalSec": "Intervallo (Secondi)", + "settings_locationIntervalInvalid": "L'intervallo deve essere di almeno 60 secondi e inferiore a 86400 secondi." } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index bedd407..cfe77e2 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -465,7 +465,7 @@ abstract class AppLocalizations { /// No description provided for @settings_locationUpdated. /// /// In en, this message translates to: - /// **'Location updated'** + /// **'GPS settings updated.'** String get settings_locationUpdated; /// No description provided for @settings_locationBothRequired. @@ -480,6 +480,30 @@ abstract class AppLocalizations { /// **'Invalid latitude or longitude.'** String get settings_locationInvalid; + /// No description provided for @settings_locationGPSEnable. + /// + /// In en, this message translates to: + /// **'GPS Enable'** + String get settings_locationGPSEnable; + + /// No description provided for @settings_locationGPSEnableSubtitle. + /// + /// In en, this message translates to: + /// **'Enable GPS to automatically send location data (if supported)'** + String get settings_locationGPSEnableSubtitle; + + /// No description provided for @settings_locationIntervalSec. + /// + /// In en, this message translates to: + /// **'Interval (Seconds)'** + String get settings_locationIntervalSec; + + /// No description provided for @settings_locationIntervalInvalid. + /// + /// In en, this message translates to: + /// **'Interval must be at least 60 seconds, and less than 86400 seconds.'** + String get settings_locationIntervalInvalid; + /// No description provided for @settings_latitude. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index cf2b35c..2b5d9c7 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -201,6 +201,20 @@ class AppLocalizationsBg extends AppLocalizations { @override String get settings_locationInvalid => 'Невалидна ширина или дължина.'; + @override + String get settings_locationGPSEnable => 'Активиране на GPS'; + + @override + String get settings_locationGPSEnableSubtitle => + 'Активирайте GPS, за автоматично изпращане на данни за местоположението (ако е поддържано).'; + + @override + String get settings_locationIntervalSec => 'Интервал (Секунди)'; + + @override + String get settings_locationIntervalInvalid => + 'Интервалът трябва да бъде поне 60 секунди и по-малко от 86400 секунди.'; + @override String get settings_latitude => 'Широчина'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 3d0cbcd..0161533 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -200,6 +200,20 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_locationInvalid => 'Ungültige Breiten- oder Längengrade.'; + @override + String get settings_locationGPSEnable => 'GPS aktivieren'; + + @override + String get settings_locationGPSEnableSubtitle => + 'Aktivieren Sie GPS, um Standortdaten automatisch zu senden (falls unterstützt).'; + + @override + String get settings_locationIntervalSec => 'Zeitintervall (Sekunden)'; + + @override + String get settings_locationIntervalInvalid => + 'Der Zeitraum muss mindestens 60 Sekunden betragen und weniger als 86400 Sekunden sein.'; + @override String get settings_latitude => 'Breitengrad'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index b8c4f90..e7ea0cf 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -190,7 +190,7 @@ class AppLocalizationsEn extends AppLocalizations { String get settings_locationSubtitle => 'GPS coordinates'; @override - String get settings_locationUpdated => 'Location updated'; + String get settings_locationUpdated => 'GPS settings updated.'; @override String get settings_locationBothRequired => @@ -199,6 +199,20 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settings_locationInvalid => 'Invalid latitude or longitude.'; + @override + String get settings_locationGPSEnable => 'GPS Enable'; + + @override + String get settings_locationGPSEnableSubtitle => + 'Enable GPS to automatically send location data (if supported)'; + + @override + String get settings_locationIntervalSec => 'Interval (Seconds)'; + + @override + String get settings_locationIntervalInvalid => + 'Interval must be at least 60 seconds, and less than 86400 seconds.'; + @override String get settings_latitude => 'Latitude'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 21aad2c..497dd2a 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -200,6 +200,20 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_locationInvalid => 'Latitud o longitud inválidos.'; + @override + String get settings_locationGPSEnable => 'Habilitar GPS'; + + @override + String get settings_locationGPSEnableSubtitle => + 'Habilitar GPS para enviar automáticamente datos de ubicación (si está disponible).'; + + @override + String get settings_locationIntervalSec => 'Intervalo (Segundos)'; + + @override + String get settings_locationIntervalInvalid => + 'El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.'; + @override String get settings_latitude => 'Latitud'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 21f63b5..50beba2 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -200,6 +200,20 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_locationInvalid => 'Latitude ou longitude invalide.'; + @override + String get settings_locationGPSEnable => 'Activer le GPS'; + + @override + String get settings_locationGPSEnableSubtitle => + 'Activer le GPS pour envoyer automatiquement les données de localisation (si pris en charge).'; + + @override + String get settings_locationIntervalSec => 'Intervalle (Secondes)'; + + @override + String get settings_locationIntervalInvalid => + 'L\'intervalle doit être d\'au moins 60 secondes et inférieur à 86400 secondes.'; + @override String get settings_latitude => 'Latitude'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 1cc567c..875e793 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -200,6 +200,20 @@ class AppLocalizationsIt extends AppLocalizations { @override String get settings_locationInvalid => 'Latitudine o longitudine non valida.'; + @override + String get settings_locationGPSEnable => 'Abilita GPS'; + + @override + String get settings_locationGPSEnableSubtitle => + 'Abilita il GPS per inviare automaticamente i dati di posizione (se supportato).'; + + @override + String get settings_locationIntervalSec => 'Intervallo (Secondi)'; + + @override + String get settings_locationIntervalInvalid => + 'L\'intervallo deve essere di almeno 60 secondi e inferiore a 86400 secondi.'; + @override String get settings_latitude => 'Latitudine'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index db1f68e..a7112bd 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -200,6 +200,20 @@ class AppLocalizationsNl extends AppLocalizations { String get settings_locationInvalid => 'Ongeldige breedtegraad of lengtegraad.'; + @override + String get settings_locationGPSEnable => 'GPS inschakelen'; + + @override + String get settings_locationGPSEnableSubtitle => + 'Zijze GPS inschakelen om locatiegegevens automatisch te verzenden (indien ondersteund).'; + + @override + String get settings_locationIntervalSec => 'Interval (Seconden)'; + + @override + String get settings_locationIntervalInvalid => + 'De intervallen moeten minstens 60 seconden zijn en minder dan 86400 seconden.'; + @override String get settings_latitude => 'Breedtegraad'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index e4373d1..3e727c0 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -202,6 +202,20 @@ class AppLocalizationsPl extends AppLocalizations { String get settings_locationInvalid => 'Nieprawidłowa szerokość geograficzna lub długość geograficzna.'; + @override + String get settings_locationGPSEnable => 'Włącz GPS'; + + @override + String get settings_locationGPSEnableSubtitle => + 'Włącz GPS, aby automatycznie wysyłać dane o lokalizacji (jeśli jest obsługiwane).'; + + @override + String get settings_locationIntervalSec => 'Interwał (Sekundy)'; + + @override + String get settings_locationIntervalInvalid => + 'Interwał musi wynosić co najmniej 60 sekund i mniej niż 86400 sekund.'; + @override String get settings_latitude => 'Szerokość'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 2aaa940..769853e 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -201,6 +201,20 @@ class AppLocalizationsPt extends AppLocalizations { @override String get settings_locationInvalid => 'Latitude ou longitude inválidos.'; + @override + String get settings_locationGPSEnable => 'Ativar GPS'; + + @override + String get settings_locationGPSEnableSubtitle => + 'Habilite o GPS para enviar dados de localização automaticamente (se suportado).'; + + @override + String get settings_locationIntervalSec => 'Intervalo (Segundos)'; + + @override + String get settings_locationIntervalInvalid => + 'O intervalo deve ser de pelo menos 60 segundos e inferior a 86400 segundos.'; + @override String get settings_latitude => 'Latitude'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index ffd327a..aac0ced 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -200,6 +200,20 @@ class AppLocalizationsSk extends AppLocalizations { @override String get settings_locationInvalid => 'Neplatná šírka alebo dĺžka.'; + @override + String get settings_locationGPSEnable => 'Aktivovať GPS'; + + @override + String get settings_locationGPSEnableSubtitle => + 'Zapnite GPS na automatické posielanie dát o polohe (ak je podporované).'; + + @override + String get settings_locationIntervalSec => 'Interval (Sekundy)'; + + @override + String get settings_locationIntervalInvalid => + 'Interval musí byť aspoň 60 sekúnd a menej ako 86400 sekúnd.'; + @override String get settings_latitude => 'Súradnica'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 994b7fa..61e4033 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -200,6 +200,20 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_locationInvalid => 'Neveljna zemeljska širina ali dolžina.'; + @override + String get settings_locationGPSEnable => 'Omogoči GPS'; + + @override + String get settings_locationGPSEnableSubtitle => + 'Omogoči GPS za samodejno pošiljanje podatkov o lokaciji (če je podprto).'; + + @override + String get settings_locationIntervalSec => 'Interval (Sekunde)'; + + @override + String get settings_locationIntervalInvalid => + 'Intervallo mora biti vsaj 60 sekund in manj kot 86400 sekund.'; + @override String get settings_latitude => 'Širina'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index a4765a9..2348e95 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -199,6 +199,20 @@ class AppLocalizationsSv extends AppLocalizations { @override String get settings_locationInvalid => 'Ogiltig latitud eller longitud.'; + @override + String get settings_locationGPSEnable => 'Aktivera GPS'; + + @override + String get settings_locationGPSEnableSubtitle => + 'Aktivera GPS för att automatiskt skicka platsdata (om det stöds).'; + + @override + String get settings_locationIntervalSec => 'Tidsintervall (Sekunder)'; + + @override + String get settings_locationIntervalInvalid => + 'Intervalet måste vara minst 60 sekunder och mindre än 86400 sekunder.'; + @override String get settings_latitude => 'Latitud'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 99f4e90..c68a38b 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -196,6 +196,18 @@ class AppLocalizationsZh extends AppLocalizations { @override String get settings_locationInvalid => '无效的纬度或经度。'; + @override + String get settings_locationGPSEnable => '启用GPS'; + + @override + String get settings_locationGPSEnableSubtitle => '启用GPS自动发送位置数据(如果支持)。'; + + @override + String get settings_locationIntervalSec => '时间间隔(秒)'; + + @override + String get settings_locationIntervalInvalid => '时间间隔必须至少为60秒,且小于86400秒。'; + @override String get settings_latitude => '纬度'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index b7e1a35..a0ddfe8 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Inloggen mislukt. Het wachtwoord is onjuist of de repeater is niet bereikbaar.", - "common_reload": "Opnieuw laden", "common_clear": "Schoonmaken", "path_currentPath": "Huidige pad: {path}", @@ -1349,5 +1348,9 @@ "channels_scanQrCode": "Scan een QR-code", "channels_scanQrCodeComingSoon": "Komt later", "channels_enterHashtag": "Voer hashtag in", - "channels_hashtagHint": "bijv. #team" + "channels_hashtagHint": "bijv. #team", + "settings_locationGPSEnable": "GPS inschakelen", + "settings_locationGPSEnableSubtitle": "Zijze GPS inschakelen om locatiegegevens automatisch te verzenden (indien ondersteund).", + "settings_locationIntervalInvalid": "De intervallen moeten minstens 60 seconden zijn en minder dan 86400 seconden.", + "settings_locationIntervalSec": "Interval (Seconden)" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 56fe869..23938a5 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Logowanie nie powiodło się. Hasło jest nieprawidłowe albo repeater jest nieosiągalny.", - "common_reload": "Ponownie załadować", "common_clear": "Wyczyść", "path_currentPath": "Aktualny ścieżka: {path}", @@ -1349,5 +1348,9 @@ "channels_scanQrCode": "Skanuj kod QR", "channels_scanQrCodeComingSoon": "Wkrótce", "channels_enterHashtag": "Wprowadź hashtag", - "channels_hashtagHint": "np. #zespół" + "channels_hashtagHint": "np. #zespół", + "settings_locationGPSEnable": "Włącz GPS", + "settings_locationGPSEnableSubtitle": "Włącz GPS, aby automatycznie wysyłać dane o lokalizacji (jeśli jest obsługiwane).", + "settings_locationIntervalSec": "Interwał (Sekundy)", + "settings_locationIntervalInvalid": "Interwał musi wynosić co najmniej 60 sekund i mniej niż 86400 sekund." } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 99da374..b49f286 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Falha no login. A senha está incorreta ou o repetidor está inacessível.", - "common_reload": "Recarregar", "common_clear": "Limpar", "path_currentPath": "Caminho atual: {path}", @@ -1349,5 +1348,9 @@ "channels_scanQrCode": "Digitalizar um Código QR", "channels_scanQrCodeComingSoon": "Em breve", "channels_enterHashtag": "Insira hashtag", - "channels_hashtagHint": "ex. #equipe" + "channels_hashtagHint": "ex. #equipe", + "settings_locationGPSEnable": "Ativar GPS", + "settings_locationGPSEnableSubtitle": "Habilite o GPS para enviar dados de localização automaticamente (se suportado).", + "settings_locationIntervalSec": "Intervalo (Segundos)", + "settings_locationIntervalInvalid": "O intervalo deve ser de pelo menos 60 segundos e inferior a 86400 segundos." } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 1ffb197..aa242c3 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Prihlásenie zlyhalo. Heslo je nesprávne alebo je opakovač nedostupný.", - "common_reload": "Načítať", "common_clear": "Zmazať", "path_currentPath": "Aktívna cesta: {path}", @@ -1349,5 +1348,9 @@ "channels_scanQrCode": "Skenujte QR kód", "channels_scanQrCodeComingSoon": "Čoskoro", "channels_enterHashtag": "Zadajte hashtag", - "channels_hashtagHint": "napr. #tím" + "channels_hashtagHint": "napr. #tím", + "settings_locationGPSEnable": "Aktivovať GPS", + "settings_locationGPSEnableSubtitle": "Zapnite GPS na automatické posielanie dát o polohe (ak je podporované).", + "settings_locationIntervalSec": "Interval (Sekundy)", + "settings_locationIntervalInvalid": "Interval musí byť aspoň 60 sekúnd a menej ako 86400 sekúnd." } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index bac8b2b..e078d40 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Prijava je bila neuspešna. Geslo je napačno ali pa je repetitor nedosegljiv.", - "common_reload": "Ponovno naloži", "common_clear": "Ponoviti", "path_currentPath": "Trenutna pot: {path}", @@ -1349,5 +1348,9 @@ "channels_scanQrCode": "Skeniraj QR kodo", "channels_scanQrCodeComingSoon": "Prihajajoča", "channels_enterHashtag": "Vnesite hashtag", - "channels_hashtagHint": "npr. #ekipa" + "channels_hashtagHint": "npr. #ekipa", + "settings_locationGPSEnable": "Omogoči GPS", + "settings_locationGPSEnableSubtitle": "Omogoči GPS za samodejno pošiljanje podatkov o lokaciji (če je podprto).", + "settings_locationIntervalSec": "Interval (Sekunde)", + "settings_locationIntervalInvalid": "Intervallo mora biti vsaj 60 sekund in manj kot 86400 sekund." } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 3c8d470..2f580cb 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Inloggning misslyckades. Antingen är lösenordet fel eller så går det inte att nå repeatern.", - "common_reload": "Ladda om", "common_clear": "Rensa", "path_currentPath": "Nuvarande sökväg: {path}", @@ -1349,5 +1348,9 @@ "channels_scanQrCode": "Skanna en QR-kod", "channels_scanQrCodeComingSoon": "Kommer snart", "channels_enterHashtag": "Ange hashtag", - "channels_hashtagHint": "t.ex. #team" + "channels_hashtagHint": "t.ex. #team", + "settings_locationGPSEnable": "Aktivera GPS", + "settings_locationIntervalSec": "Tidsintervall (Sekunder)", + "settings_locationGPSEnableSubtitle": "Aktivera GPS för att automatiskt skicka platsdata (om det stöds).", + "settings_locationIntervalInvalid": "Intervalet måste vara minst 60 sekunder och mindre än 86400 sekunder." } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index fc5ac55..fa9e64d 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "登录失败。密码不正确或中继器不可达。", - "common_reload": "重新加载", "common_clear": "清除", "path_currentPath": "当前路径:{path}", @@ -1349,5 +1348,9 @@ "channels_scanQrCode": "扫描二维码", "channels_scanQrCodeComingSoon": "即将到来", "channels_enterHashtag": "输入标签", - "channels_hashtagHint": "例如 #团队" + "channels_hashtagHint": "例如 #团队", + "settings_locationGPSEnableSubtitle": "启用GPS自动发送位置数据(如果支持)。", + "settings_locationGPSEnable": "启用GPS", + "settings_locationIntervalSec": "时间间隔(秒)", + "settings_locationIntervalInvalid": "时间间隔必须至少为60秒,且小于86400秒。" } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 8e23825..d925c30 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -38,10 +38,7 @@ class _SettingsScreenState extends State { Widget build(BuildContext context) { final l10n = context.l10n; return Scaffold( - appBar: AppBar( - title: Text(l10n.settings_title), - centerTitle: true, - ), + appBar: AppBar(title: Text(l10n.settings_title), centerTitle: true), body: SafeArea( top: false, child: Consumer( @@ -68,7 +65,10 @@ class _SettingsScreenState extends State { ); } - Widget _buildDeviceInfoCard(BuildContext context, MeshCoreConnector connector) { + Widget _buildDeviceInfoCard( + BuildContext context, + MeshCoreConnector connector, + ) { final l10n = context.l10n; return Card( child: Padding( @@ -83,21 +83,38 @@ class _SettingsScreenState extends State { const SizedBox(height: 16), _buildInfoRow(l10n.settings_infoName, connector.deviceDisplayName), _buildInfoRow(l10n.settings_infoId, connector.deviceIdLabel), - _buildInfoRow(l10n.settings_infoStatus, connector.isConnected ? l10n.common_connected : l10n.common_disconnected), + _buildInfoRow( + l10n.settings_infoStatus, + connector.isConnected + ? l10n.common_connected + : l10n.common_disconnected, + ), _buildBatteryInfoRow(context, connector), if (connector.selfName != null) _buildInfoRow(l10n.settings_nodeName, connector.selfName!), if (connector.selfPublicKey != null) - _buildInfoRow(l10n.settings_infoPublicKey, '${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...'), - _buildInfoRow(l10n.settings_infoContactsCount, '${connector.contacts.length}'), - _buildInfoRow(l10n.settings_infoChannelCount, '${connector.channels.length}'), + _buildInfoRow( + l10n.settings_infoPublicKey, + '${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...', + ), + _buildInfoRow( + l10n.settings_infoContactsCount, + '${connector.contacts.length}', + ), + _buildInfoRow( + l10n.settings_infoChannelCount, + '${connector.channels.length}', + ), ], ), ), ); } - Widget _buildBatteryInfoRow(BuildContext context, MeshCoreConnector connector) { + Widget _buildBatteryInfoRow( + BuildContext context, + MeshCoreConnector connector, + ) { final l10n = context.l10n; final percent = connector.batteryPercent; final millivolts = connector.batteryMillivolts; @@ -167,7 +184,10 @@ class _SettingsScreenState extends State { ); } - Widget _buildNodeSettingsCard(BuildContext context, MeshCoreConnector connector) { + Widget _buildNodeSettingsCard( + BuildContext context, + MeshCoreConnector connector, + ) { final l10n = context.l10n; return Card( child: Column( @@ -298,7 +318,9 @@ class _SettingsScreenState extends State { onTap: () { Navigator.push( context, - MaterialPageRoute(builder: (context) => const BleDebugLogScreen()), + MaterialPageRoute( + builder: (context) => const BleDebugLogScreen(), + ), ); }, ), @@ -311,7 +333,9 @@ class _SettingsScreenState extends State { onTap: () { Navigator.push( context, - MaterialPageRoute(builder: (context) => const AppDebugLogScreen()), + MaterialPageRoute( + builder: (context) => const AppDebugLogScreen(), + ), ); }, ), @@ -334,20 +358,14 @@ class _SettingsScreenState extends State { children: [ Row( children: [ - if (leading != null) ...[ - leading, - const SizedBox(width: 8), - ], + if (leading != null) ...[leading, const SizedBox(width: 8)], Text(label, style: TextStyle(color: Colors.grey[600])), ], ), Flexible( child: Text( value, - style: TextStyle( - fontWeight: FontWeight.w500, - color: valueColor, - ), + style: TextStyle(fontWeight: FontWeight.w500, color: valueColor), overflow: TextOverflow.ellipsis, ), ), @@ -413,75 +431,152 @@ class _SettingsScreenState extends State { final l10n = context.l10n; final latController = TextEditingController(); final lonController = TextEditingController(); + final intervalController = TextEditingController(); + intervalController.text = "900"; + latController.text = connector.selfLatitude?.toStringAsFixed(6) ?? ''; + lonController.text = connector.selfLongitude?.toStringAsFixed(6) ?? ''; + bool isGPSEnabled = false; + showDialog( context: context, - builder: (context) => AlertDialog( - title: Text(l10n.settings_location), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: latController, - decoration: InputDecoration( - labelText: l10n.settings_latitude, - border: const OutlineInputBorder(), + builder: (dialogContext) => StatefulBuilder( + builder: (context, setDialogState) => AlertDialog( + title: Text(l10n.settings_location), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (!isGPSEnabled) ...[ + TextField( + controller: latController, + decoration: InputDecoration( + labelText: l10n.settings_latitude, + border: const OutlineInputBorder(), + ), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + signed: true, + ), + ), + const SizedBox(height: 16), + TextField( + controller: lonController, + decoration: InputDecoration( + labelText: l10n.settings_longitude, + border: const OutlineInputBorder(), + ), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + signed: true, + ), + ), + ], + const SizedBox(height: 16), + CheckboxListTile( + value: isGPSEnabled, + enabled: true, + onChanged: (v) => + setDialogState(() => isGPSEnabled = v ?? false), + //controlAffinity: ListTileControlAffinity.leading, + title: Text( + l10n.settings_locationGPSEnable, + style: TextStyle(fontSize: 12), + ), + subtitle: Text( + l10n.settings_locationGPSEnableSubtitle, + style: TextStyle(fontSize: 10), + ), ), - keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), + if (isGPSEnabled) ...{ + const SizedBox(height: 16), + TextField( + controller: intervalController, + decoration: InputDecoration( + labelText: l10n.settings_locationIntervalSec, + border: const OutlineInputBorder(), + ), + keyboardType: const TextInputType.numberWithOptions( + decimal: false, + signed: false, + ), + ), + }, + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(l10n.common_cancel), ), - const SizedBox(height: 16), - TextField( - controller: lonController, - decoration: InputDecoration( - labelText: l10n.settings_longitude, - border: const OutlineInputBorder(), - ), - keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), + TextButton( + onPressed: () async { + Navigator.pop(context); + if (isGPSEnabled) { + final intervalText = intervalController.text.trim(); + if (intervalText.isEmpty) { + return; + } + final interval = int.tryParse(intervalText); + if (interval == null || interval < 60) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(l10n.settings_locationIntervalInvalid), + ), + ); + return; + } + await connector.setCustomVar("gps:1"); + await connector.setCustomVar("gps_interval:$interval"); + await connector.refreshDeviceInfo(); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.settings_locationUpdated)), + ); + } else { + final latText = latController.text.trim(); + final lonText = lonController.text.trim(); + if (latText.isEmpty && lonText.isEmpty) { + return; + } + + final currentLat = connector.selfLatitude; + final currentLon = connector.selfLongitude; + final lat = latText.isNotEmpty + ? double.tryParse(latText) + : currentLat; + final lon = lonText.isNotEmpty + ? double.tryParse(lonText) + : currentLon; + if (lat == null || lon == null) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(l10n.settings_locationBothRequired), + ), + ); + return; + } + if (lat < -90 || lat > 90 || lon < -180 || lon > 180) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.settings_locationInvalid)), + ); + return; + } + + await connector.setCustomVar("gps:0"); + await connector.setNodeLocation(lat: lat, lon: lon); + await connector.refreshDeviceInfo(); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.settings_locationUpdated)), + ); + } + }, + child: Text(l10n.common_save), ), ], ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(l10n.common_cancel), - ), - TextButton( - onPressed: () async { - Navigator.pop(context); - final latText = latController.text.trim(); - final lonText = lonController.text.trim(); - if (latText.isEmpty && lonText.isEmpty) { - return; - } - - final currentLat = connector.selfLatitude; - final currentLon = connector.selfLongitude; - final lat = latText.isNotEmpty ? double.tryParse(latText) : currentLat; - final lon = lonText.isNotEmpty ? double.tryParse(lonText) : currentLon; - if (lat == null || lon == null) { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_locationBothRequired)), - ); - return; - } - if (lat < -90 || lat > 90 || lon < -180 || lon > 180) { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_locationInvalid)), - ); - return; - } - - await connector.setNodeLocation(lat: lat, lon: lon); - await connector.refreshDeviceInfo(); - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_locationUpdated)), - ); - }, - child: Text(l10n.common_save), - ), - ], ), ); } @@ -530,17 +625,17 @@ class _SettingsScreenState extends State { void _sendAdvert(BuildContext context, MeshCoreConnector connector) { final l10n = context.l10n; connector.sendSelfAdvert(flood: true); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_advertisementSent)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.settings_advertisementSent))); } void _syncTime(BuildContext context, MeshCoreConnector connector) { final l10n = context.l10n; connector.syncTime(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_timeSynchronized)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.settings_timeSynchronized))); } void _confirmReboot(BuildContext context, MeshCoreConnector connector) { @@ -560,7 +655,10 @@ class _SettingsScreenState extends State { Navigator.pop(context); connector.rebootDevice(); }, - child: Text(l10n.common_reboot, style: const TextStyle(color: Colors.orange)), + child: Text( + l10n.common_reboot, + style: const TextStyle(color: Colors.orange), + ), ), ], ), @@ -572,7 +670,9 @@ class _SettingsScreenState extends State { showAboutDialog( context: context, applicationName: l10n.appTitle, - applicationVersion: _appVersion.isEmpty ? l10n.common_loading : _appVersion, + applicationVersion: _appVersion.isEmpty + ? l10n.common_loading + : _appVersion, applicationLegalese: l10n.settings_aboutLegalese, children: [ const SizedBox(height: 16), @@ -604,7 +704,8 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { // Populate with current settings if available if (widget.connector.currentFreqHz != null) { - _frequencyController.text = (widget.connector.currentFreqHz! / 1000.0).toStringAsFixed(3); + _frequencyController.text = (widget.connector.currentFreqHz! / 1000.0) + .toStringAsFixed(3); } else { _frequencyController.text = '915.0'; } @@ -670,26 +771,31 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { final txPower = int.tryParse(_txPowerController.text); if (freqMHz == null || freqMHz < 300 || freqMHz > 2500) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_frequencyInvalid)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.settings_frequencyInvalid))); return; } if (txPower == null || txPower < 0 || txPower > 22) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_txPowerInvalid)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.settings_txPowerInvalid))); return; } final freqHz = (freqMHz * 1000).round(); final bwHz = _bandwidth.hz; final sf = _spreadingFactor.value; - final cr = _toDeviceCodingRate(_codingRate.value, widget.connector.currentCr); + final cr = _toDeviceCodingRate( + _codingRate.value, + widget.connector.currentCr, + ); try { - await widget.connector.sendFrame(buildSetRadioParamsFrame(freqHz, bwHz, sf, cr)); + await widget.connector.sendFrame( + buildSetRadioParamsFrame(freqHz, bwHz, sf, cr), + ); await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower)); await widget.connector.refreshDeviceInfo(); @@ -727,7 +833,10 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(l10n.settings_presets, style: const TextStyle(fontWeight: FontWeight.bold)), + Text( + l10n.settings_presets, + style: const TextStyle(fontWeight: FontWeight.bold), + ), const SizedBox(height: 8), Wrap( spacing: 8, @@ -762,7 +871,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { border: const OutlineInputBorder(), helperText: l10n.settings_frequencyHelper, ), - keyboardType: const TextInputType.numberWithOptions(decimal: true), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), ), const SizedBox(height: 16), DropdownButtonFormField( @@ -772,10 +883,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { border: const OutlineInputBorder(), ), items: LoRaBandwidth.values - .map((bw) => DropdownMenuItem( - value: bw, - child: Text(bw.label), - )) + .map( + (bw) => DropdownMenuItem(value: bw, child: Text(bw.label)), + ) .toList(), onChanged: (value) { if (value != null) setState(() => _bandwidth = value); @@ -789,10 +899,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { border: const OutlineInputBorder(), ), items: LoRaSpreadingFactor.values - .map((sf) => DropdownMenuItem( - value: sf, - child: Text(sf.label), - )) + .map( + (sf) => DropdownMenuItem(value: sf, child: Text(sf.label)), + ) .toList(), onChanged: (value) { if (value != null) setState(() => _spreadingFactor = value); @@ -806,10 +915,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { border: const OutlineInputBorder(), ), items: LoRaCodingRate.values - .map((cr) => DropdownMenuItem( - value: cr, - child: Text(cr.label), - )) + .map( + (cr) => DropdownMenuItem(value: cr, child: Text(cr.label)), + ) .toList(), onChanged: (value) { if (value != null) setState(() => _codingRate = value); @@ -833,10 +941,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { onPressed: () => Navigator.pop(context), child: Text(l10n.common_cancel), ), - FilledButton( - onPressed: _saveSettings, - child: Text(l10n.common_save), - ), + FilledButton(onPressed: _saveSettings, child: Text(l10n.common_save)), ], ); } @@ -850,9 +955,6 @@ class _PresetChip extends StatelessWidget { @override Widget build(BuildContext context) { - return ActionChip( - label: Text(label), - onPressed: onTap, - ); + return ActionChip(label: Text(label), onPressed: onTap); } } From 153736d36e2f6f375f871f7324df31ebd1b2513b Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 18 Jan 2026 21:21:33 -0800 Subject: [PATCH 05/11] added roomserver management --- lib/l10n/app_bg.arb | 5 +- lib/l10n/app_de.arb | 5 +- lib/l10n/app_en.arb | 6 +- lib/l10n/app_es.arb | 5 +- lib/l10n/app_fr.arb | 5 +- lib/l10n/app_it.arb | 5 +- lib/l10n/app_localizations.dart | 16 +- lib/l10n/app_localizations_bg.dart | 6 + lib/l10n/app_localizations_de.dart | 6 + lib/l10n/app_localizations_en.dart | 10 +- lib/l10n/app_localizations_es.dart | 6 + lib/l10n/app_localizations_fr.dart | 6 + lib/l10n/app_localizations_it.dart | 6 + lib/l10n/app_localizations_nl.dart | 6 + lib/l10n/app_localizations_pl.dart | 6 + lib/l10n/app_localizations_pt.dart | 6 + lib/l10n/app_localizations_sk.dart | 6 + lib/l10n/app_localizations_sl.dart | 6 + lib/l10n/app_localizations_sv.dart | 6 + lib/l10n/app_localizations_zh.dart | 6 + lib/l10n/app_nl.arb | 5 +- lib/l10n/app_pl.arb | 5 +- lib/l10n/app_pt.arb | 5 +- lib/l10n/app_sk.arb | 5 +- lib/l10n/app_sl.arb | 5 +- lib/l10n/app_sv.arb | 5 +- lib/l10n/app_zh.arb | 5 +- lib/screens/contacts_screen.dart | 250 +++++++++++++++++-------- lib/screens/repeater_hub_screen.dart | 269 ++++++++++++++------------- 29 files changed, 447 insertions(+), 236 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 30a2c1b..1f0f671 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Входът не беше успешен. Или паролата е грешна, или повторителят е недостъпен.", - "common_reload": "Презареди", "common_clear": "Изчисти", "path_currentPath": "Текущ път: {path}", @@ -1349,5 +1348,7 @@ "channels_scanQrCode": "Сканирайте QR код", "channels_scanQrCodeComingSoon": "Ще излезе скоро", "channels_enterHashtag": "Въведете хаштаг", - "channels_hashtagHint": "напр. #отбор" + "channels_hashtagHint": "напр. #отбор", + "room_management": "Управление на сървъра за стая", + "contacts_manageRoom": "Управление на сървър за стая" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 9d2701f..8b76346 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Anmeldung fehlgeschlagen. Entweder ist das Passwort falsch oder der Repeater ist nicht erreichbar.", - "common_reload": "Neu laden", "common_clear": "Löschen", "path_currentPath": "Aktiver Pfad: {path}", @@ -1349,5 +1348,7 @@ "channels_scanQrCode": "Scannen Sie einen QR-Code", "channels_scanQrCodeComingSoon": "Bald verfügbar", "channels_enterHashtag": "Gib Hashtag ein", - "channels_hashtagHint": "z.B. #team" + "channels_hashtagHint": "z.B. #team", + "contacts_manageRoom": "Verwalten Sie den Raumserver", + "room_management": "Raumserververwaltung" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 203f64f..cf33b6c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -253,7 +253,8 @@ } }, "contacts_manageRepeater": "Manage Repeater", - "contacts_roomLogin": "Room Login", + "contacts_manageRoom": "Manage Room Server", + "contacts_roomLogin": "Room Server Login", "contacts_openChat": "Open Chat", "contacts_editGroup": "Edit Group", "contacts_deleteGroup": "Delete Group", @@ -697,7 +698,7 @@ "dialog_disconnectConfirm": "Are you sure you want to disconnect from this device?", "login_repeaterLogin": "Repeater Login", - "login_roomLogin": "Room Login", + "login_roomLogin": "Room Server Login", "login_password": "Password", "login_enterPassword": "Enter password", "login_savePassword": "Save password", @@ -760,6 +761,7 @@ "path_setPath": "Set Path", "repeater_management": "Repeater Management", + "room_management": "Room Server Management", "repeater_managementTools": "Management Tools", "repeater_status": "Status", "repeater_statusSubtitle": "View repeater status, stats, and neighbors", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index b23cfce..af604e1 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Inicio fallido. La contraseña es incorrecta o el repetidor no está disponible.", - "common_reload": "Recargar", "common_clear": "Borrar", "path_currentPath": "Ruta actual: {path}", @@ -1349,5 +1348,7 @@ "channels_scanQrCode": "Escanear un Código QR", "channels_scanQrCodeComingSoon": "Próximamente", "channels_enterHashtag": "Introducir hashtag", - "channels_hashtagHint": "ej. #equipo" + "channels_hashtagHint": "ej. #equipo", + "contacts_manageRoom": "Gestionar Servidor de Habitación", + "room_management": "Administración del Servidor de Habitación" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 0e7b952..8d14b8f 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Connexion échouée. Soit le mot de passe est incorrect, soit le relais est injoignable.", - "common_reload": "Recharger", "common_clear": "Effacer", "path_currentPath": "Chemin actuel : {path}", @@ -1349,5 +1348,7 @@ "channels_scanQrCode": "Scanner un code QR", "channels_scanQrCodeComingSoon": "Bientôt disponible", "channels_enterHashtag": "Entrez le hashtag", - "channels_hashtagHint": "ex. #équipe" + "channels_hashtagHint": "ex. #équipe", + "contacts_manageRoom": "Gérer le serveur de la pièce {name}", + "room_management": "Gestion du serveur de pièce" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index b77450f..ea8c6ac 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.", - "common_reload": "Ricaricare", "common_clear": "Cancella", "path_currentPath": "Percorso corrente: {path}", @@ -1349,5 +1348,7 @@ "channels_scanQrCode": "Scansiona un codice QR", "channels_scanQrCodeComingSoon": "Arriverà presto", "channels_enterHashtag": "Inserisci hashtag", - "channels_hashtagHint": "es. #team" + "channels_hashtagHint": "es. #team", + "room_management": "Gestione del Server di Camera", + "contacts_manageRoom": "Gestisci Server Camera" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index bedd407..22c13f8 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1284,10 +1284,16 @@ abstract class AppLocalizations { /// **'Manage Repeater'** String get contacts_manageRepeater; + /// No description provided for @contacts_manageRoom. + /// + /// In en, this message translates to: + /// **'Manage Room Server'** + String get contacts_manageRoom; + /// No description provided for @contacts_roomLogin. /// /// In en, this message translates to: - /// **'Room Login'** + /// **'Room Server Login'** String get contacts_roomLogin; /// No description provided for @contacts_openChat. @@ -2672,7 +2678,7 @@ abstract class AppLocalizations { /// No description provided for @login_roomLogin. /// /// In en, this message translates to: - /// **'Room Login'** + /// **'Room Server Login'** String get login_roomLogin; /// No description provided for @login_password. @@ -2867,6 +2873,12 @@ abstract class AppLocalizations { /// **'Repeater Management'** String get repeater_management; + /// No description provided for @room_management. + /// + /// In en, this message translates to: + /// **'Room Server Management'** + String get room_management; + /// No description provided for @repeater_managementTools. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index cf2b35c..0c666fa 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -650,6 +650,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get contacts_manageRepeater => 'Управление на Повтарящ се Елемент'; + @override + String get contacts_manageRoom => 'Manage Room Server'; + @override String get contacts_roomLogin => 'Вход в стаята'; @@ -1587,6 +1590,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get repeater_management => 'Управление на повторители'; + @override + String get room_management => 'Room Server Management'; + @override String get repeater_managementTools => 'Инструменти за управление'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 3d0cbcd..d3edf4e 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -647,6 +647,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get contacts_manageRepeater => 'Wiederholungen verwalten'; + @override + String get contacts_manageRoom => 'Manage Room Server'; + @override String get contacts_roomLogin => 'Raum-Login'; @@ -1586,6 +1589,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_management => 'Repeater-Verwaltung'; + @override + String get room_management => 'Room Server Management'; + @override String get repeater_managementTools => 'Verwaltungs-Tools'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index b8c4f90..3051061 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -641,7 +641,10 @@ class AppLocalizationsEn extends AppLocalizations { String get contacts_manageRepeater => 'Manage Repeater'; @override - String get contacts_roomLogin => 'Room Login'; + String get contacts_manageRoom => 'Manage Room Server'; + + @override + String get contacts_roomLogin => 'Room Server Login'; @override String get contacts_openChat => 'Open Chat'; @@ -1439,7 +1442,7 @@ class AppLocalizationsEn extends AppLocalizations { String get login_repeaterLogin => 'Repeater Login'; @override - String get login_roomLogin => 'Room Login'; + String get login_roomLogin => 'Room Server Login'; @override String get login_password => 'Password'; @@ -1561,6 +1564,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get repeater_management => 'Repeater Management'; + @override + String get room_management => 'Room Server Management'; + @override String get repeater_managementTools => 'Management Tools'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 21aad2c..9f92ba7 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -648,6 +648,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get contacts_manageRepeater => 'Gestionar Repetidor'; + @override + String get contacts_manageRoom => 'Manage Room Server'; + @override String get contacts_roomLogin => 'Inicio de Sala'; @@ -1585,6 +1588,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_management => 'Gestión de Repetidores'; + @override + String get room_management => 'Room Server Management'; + @override String get repeater_managementTools => 'Herramientas de Gestión'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 21f63b5..ced92dc 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -649,6 +649,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get contacts_manageRepeater => 'Gérer le répétiteur'; + @override + String get contacts_manageRoom => 'Manage Room Server'; + @override String get contacts_roomLogin => 'Connexion Salle'; @@ -1591,6 +1594,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_management => 'Gestion des répétiteurs'; + @override + String get room_management => 'Room Server Management'; + @override String get repeater_managementTools => 'Outils de Gestion'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 1cc567c..1351305 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -646,6 +646,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get contacts_manageRepeater => 'Gestisci Ripetitore'; + @override + String get contacts_manageRoom => 'Manage Room Server'; + @override String get contacts_roomLogin => 'Login Camera'; @@ -1583,6 +1586,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_management => 'Gestione Ripetitori'; + @override + String get room_management => 'Room Server Management'; + @override String get repeater_managementTools => 'Strumenti di Gestione'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index db1f68e..baf4668 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -644,6 +644,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get contacts_manageRepeater => 'Beheer Repeater'; + @override + String get contacts_manageRoom => 'Manage Room Server'; + @override String get contacts_roomLogin => 'Ruimte Inloggen'; @@ -1578,6 +1581,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get repeater_management => 'Beheer Repeaters'; + @override + String get room_management => 'Room Server Management'; + @override String get repeater_managementTools => 'Beheerinstrumenten'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index e4373d1..5051b83 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -649,6 +649,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get contacts_manageRepeater => 'Zarządzaj Powtórzami'; + @override + String get contacts_manageRoom => 'Manage Room Server'; + @override String get contacts_roomLogin => 'Logowanie do pokoju'; @@ -1587,6 +1590,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get repeater_management => 'Zarządzanie Powtórzami'; + @override + String get room_management => 'Room Server Management'; + @override String get repeater_managementTools => 'Narzędzia Zarządzania'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 2aaa940..96cb130 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -649,6 +649,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get contacts_manageRepeater => 'Gerenciar Repetidor'; + @override + String get contacts_manageRoom => 'Manage Room Server'; + @override String get contacts_roomLogin => 'Login no Quarto'; @@ -1585,6 +1588,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_management => 'Gerenciamento de Repetidor'; + @override + String get room_management => 'Room Server Management'; + @override String get repeater_managementTools => 'Ferramentas de Gerenciamento'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index ffd327a..cdb39c7 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -642,6 +642,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get contacts_manageRepeater => 'Spravovať opakované zoznamy'; + @override + String get contacts_manageRoom => 'Manage Room Server'; + @override String get contacts_roomLogin => 'Prihlásenie do miestnosti'; @@ -1580,6 +1583,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get repeater_management => 'Správa opakérov'; + @override + String get room_management => 'Room Server Management'; + @override String get repeater_managementTools => 'Nástroje na správu'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 994b7fa..a54282c 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -644,6 +644,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get contacts_manageRepeater => 'Upravljajte Ponovitve'; + @override + String get contacts_manageRoom => 'Manage Room Server'; + @override String get contacts_roomLogin => 'Vnos v sobo'; @@ -1580,6 +1583,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_management => 'Upravljanje ponovitve'; + @override + String get room_management => 'Room Server Management'; + @override String get repeater_managementTools => 'Upravne orodje'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index a4765a9..2b595a8 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -638,6 +638,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get contacts_manageRepeater => 'Hantera Upprepare'; + @override + String get contacts_manageRoom => 'Manage Room Server'; + @override String get contacts_roomLogin => 'Rum Inloggning'; @@ -1569,6 +1572,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_management => 'Återuppspelarens Hantering'; + @override + String get room_management => 'Room Server Management'; + @override String get repeater_managementTools => 'Administrationsverktyg'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 99f4e90..2c0cddb 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -611,6 +611,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get contacts_manageRepeater => '管理重复项'; + @override + String get contacts_manageRoom => 'Manage Room Server'; + @override String get contacts_roomLogin => '房间登录'; @@ -1513,6 +1516,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get repeater_management => '重复器管理'; + @override + String get room_management => 'Room Server Management'; + @override String get repeater_managementTools => '管理工具'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index b7e1a35..fd069fc 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Inloggen mislukt. Het wachtwoord is onjuist of de repeater is niet bereikbaar.", - "common_reload": "Opnieuw laden", "common_clear": "Schoonmaken", "path_currentPath": "Huidige pad: {path}", @@ -1349,5 +1348,7 @@ "channels_scanQrCode": "Scan een QR-code", "channels_scanQrCodeComingSoon": "Komt later", "channels_enterHashtag": "Voer hashtag in", - "channels_hashtagHint": "bijv. #team" + "channels_hashtagHint": "bijv. #team", + "room_management": "Beheer Server Kamer", + "contacts_manageRoom": "Beheer Ruimte Server" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 56fe869..60a6ab5 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Logowanie nie powiodło się. Hasło jest nieprawidłowe albo repeater jest nieosiągalny.", - "common_reload": "Ponownie załadować", "common_clear": "Wyczyść", "path_currentPath": "Aktualny ścieżka: {path}", @@ -1349,5 +1348,7 @@ "channels_scanQrCode": "Skanuj kod QR", "channels_scanQrCodeComingSoon": "Wkrótce", "channels_enterHashtag": "Wprowadź hashtag", - "channels_hashtagHint": "np. #zespół" + "channels_hashtagHint": "np. #zespół", + "contacts_manageRoom": "Zarządzaj Serwerem Pokoju", + "room_management": "Zarządzanie Serwerem Pokoju" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 99da374..041b479 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Falha no login. A senha está incorreta ou o repetidor está inacessível.", - "common_reload": "Recarregar", "common_clear": "Limpar", "path_currentPath": "Caminho atual: {path}", @@ -1349,5 +1348,7 @@ "channels_scanQrCode": "Digitalizar um Código QR", "channels_scanQrCodeComingSoon": "Em breve", "channels_enterHashtag": "Insira hashtag", - "channels_hashtagHint": "ex. #equipe" + "channels_hashtagHint": "ex. #equipe", + "contacts_manageRoom": "Gerenciar Servidor de Sala {name} ({count})", + "room_management": "Gerenciamento de Servidor de Sala" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 1ffb197..688a1bb 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Prihlásenie zlyhalo. Heslo je nesprávne alebo je opakovač nedostupný.", - "common_reload": "Načítať", "common_clear": "Zmazať", "path_currentPath": "Aktívna cesta: {path}", @@ -1349,5 +1348,7 @@ "channels_scanQrCode": "Skenujte QR kód", "channels_scanQrCodeComingSoon": "Čoskoro", "channels_enterHashtag": "Zadajte hashtag", - "channels_hashtagHint": "napr. #tím" + "channels_hashtagHint": "napr. #tím", + "room_management": "Správa servera miestnosti", + "contacts_manageRoom": "Spravovať server miestnosti" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index bac8b2b..abd55cc 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Prijava je bila neuspešna. Geslo je napačno ali pa je repetitor nedosegljiv.", - "common_reload": "Ponovno naloži", "common_clear": "Ponoviti", "path_currentPath": "Trenutna pot: {path}", @@ -1349,5 +1348,7 @@ "channels_scanQrCode": "Skeniraj QR kodo", "channels_scanQrCodeComingSoon": "Prihajajoča", "channels_enterHashtag": "Vnesite hashtag", - "channels_hashtagHint": "npr. #ekipa" + "channels_hashtagHint": "npr. #ekipa", + "contacts_manageRoom": "Upravljajte strežnik sobe", + "room_management": "Upravljanje stremlišča" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 3c8d470..9b70a3e 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Inloggning misslyckades. Antingen är lösenordet fel eller så går det inte att nå repeatern.", - "common_reload": "Ladda om", "common_clear": "Rensa", "path_currentPath": "Nuvarande sökväg: {path}", @@ -1349,5 +1348,7 @@ "channels_scanQrCode": "Skanna en QR-kod", "channels_scanQrCodeComingSoon": "Kommer snart", "channels_enterHashtag": "Ange hashtag", - "channels_hashtagHint": "t.ex. #team" + "channels_hashtagHint": "t.ex. #team", + "contacts_manageRoom": "Hantera Rumserver", + "room_management": "Rumserverhantering" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index fc5ac55..56580fa 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "登录失败。密码不正确或中继器不可达。", - "common_reload": "重新加载", "common_clear": "清除", "path_currentPath": "当前路径:{path}", @@ -1349,5 +1348,7 @@ "channels_scanQrCode": "扫描二维码", "channels_scanQrCodeComingSoon": "即将到来", "channels_enterHashtag": "输入标签", - "channels_hashtagHint": "例如 #团队" + "channels_hashtagHint": "例如 #团队", + "contacts_manageRoom": "管理房间服务器", + "room_management": "房间服务器管理" } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index b9ceb84..8d15cc3 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -30,10 +30,7 @@ import 'settings_screen.dart'; class ContactsScreen extends StatefulWidget { final bool hideBackButton; - const ContactsScreen({ - super.key, - this.hideBackButton = false, - }); + const ContactsScreen({super.key, this.hideBackButton = false}); @override State createState() => _ContactsScreenState(); @@ -114,7 +111,8 @@ class _ContactsScreenState extends State top: false, child: QuickSwitchBar( selectedIndex: 0, - onDestinationSelected: (index) => _handleQuickSwitch(index, context), + onDestinationSelected: (index) => + _handleQuickSwitch(index, context), ), ), ), @@ -168,8 +166,9 @@ class _ContactsScreenState extends State } final filteredAndSorted = _filterAndSortContacts(contacts, connector); - final filteredGroups = - _showUnreadOnly ? const [] : _filterAndSortGroups(_groups, contacts); + final filteredGroups = _showUnreadOnly + ? const [] + : _filterAndSortGroups(_groups, contacts); return Column( children: [ @@ -199,7 +198,10 @@ class _ContactsScreenState extends State border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), ), onChanged: (value) { _searchDebounce?.cancel(); @@ -238,14 +240,18 @@ class _ContactsScreenState extends State final group = filteredGroups[index]; return _buildGroupTile(context, group, contacts); } - final contact = filteredAndSorted[index - filteredGroups.length]; - final unreadCount = connector.getUnreadCountForContact(contact); + final contact = + filteredAndSorted[index - filteredGroups.length]; + final unreadCount = connector.getUnreadCountForContact( + contact, + ); return _ContactTile( contact: contact, lastSeen: _resolveLastSeen(contact), unreadCount: unreadCount, onTap: () => _openChat(context, contact), - onLongPress: () => _showContactOptions(context, connector, contact), + onLongPress: () => + _showContactOptions(context, connector, contact), ); }, ), @@ -255,35 +261,47 @@ class _ContactsScreenState extends State ); } - List _filterAndSortGroups(List groups, List contacts) { + List _filterAndSortGroups( + List groups, + List contacts, + ) { final query = _searchQuery.trim().toLowerCase(); final contactsByKey = {}; for (final contact in contacts) { contactsByKey[contact.publicKeyHex] = contact; } - final filtered = groups.where((group) { - if (query.isEmpty) return true; - if (group.name.toLowerCase().contains(query)) return true; - for (final key in group.memberKeys) { - final contact = contactsByKey[key]; - if (contact != null && matchesContactQuery(contact, query)) return true; - } - return false; - }).where((group) { - if (_typeFilter == ContactTypeFilter.all) return true; - for (final key in group.memberKeys) { - final contact = contactsByKey[key]; - if (contact != null && _matchesTypeFilter(contact)) return true; - } - return false; - }).toList(); + final filtered = groups + .where((group) { + if (query.isEmpty) return true; + if (group.name.toLowerCase().contains(query)) return true; + for (final key in group.memberKeys) { + final contact = contactsByKey[key]; + if (contact != null && matchesContactQuery(contact, query)) + return true; + } + return false; + }) + .where((group) { + if (_typeFilter == ContactTypeFilter.all) return true; + for (final key in group.memberKeys) { + final contact = contactsByKey[key]; + if (contact != null && _matchesTypeFilter(contact)) return true; + } + return false; + }) + .toList(); - filtered.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); + filtered.sort( + (a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()), + ); return filtered; } - List _filterAndSortContacts(List contacts, MeshCoreConnector connector) { + List _filterAndSortContacts( + List contacts, + MeshCoreConnector connector, + ) { var filtered = contacts.where((contact) { if (_searchQuery.isEmpty) return true; return matchesContactQuery(contact, _searchQuery); @@ -301,19 +319,27 @@ class _ContactsScreenState extends State switch (_sortOption) { case ContactSortOption.lastSeen: - filtered.sort((a, b) => _resolveLastSeen(b).compareTo(_resolveLastSeen(a))); + filtered.sort( + (a, b) => _resolveLastSeen(b).compareTo(_resolveLastSeen(a)), + ); break; case ContactSortOption.recentMessages: filtered.sort((a, b) { final aMessages = connector.getMessages(a); final bMessages = connector.getMessages(b); - final aLastMsg = aMessages.isEmpty ? DateTime(1970) : aMessages.last.timestamp; - final bLastMsg = bMessages.isEmpty ? DateTime(1970) : bMessages.last.timestamp; + final aLastMsg = aMessages.isEmpty + ? DateTime(1970) + : aMessages.last.timestamp; + final bLastMsg = bMessages.isEmpty + ? DateTime(1970) + : bMessages.last.timestamp; return bLastMsg.compareTo(aLastMsg); }); break; case ContactSortOption.name: - filtered.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); + filtered.sort( + (a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()), + ); break; } @@ -340,7 +366,11 @@ class _ContactsScreenState extends State : contact.lastSeen; } - Widget _buildGroupTile(BuildContext context, ContactGroup group, List contacts) { + Widget _buildGroupTile( + BuildContext context, + ContactGroup group, + List contacts, + ) { final memberContacts = _resolveGroupContacts(group, contacts); final subtitle = _formatGroupMembers(context, memberContacts); return ListTile( @@ -359,7 +389,10 @@ class _ContactsScreenState extends State ); } - List _resolveGroupContacts(ContactGroup group, List contacts) { + List _resolveGroupContacts( + ContactGroup group, + List contacts, + ) { final byKey = {}; for (final contact in contacts) { byKey[contact.publicKeyHex] = contact; @@ -371,7 +404,9 @@ class _ContactsScreenState extends State resolved.add(contact); } } - resolved.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); + resolved.sort( + (a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()), + ); return resolved; } @@ -387,7 +422,7 @@ class _ContactsScreenState extends State if (contact.type == advTypeRepeater) { _showRepeaterLogin(context, contact); } else if (contact.type == advTypeRoom) { - _showRoomLogin(context, contact); + _showRoomLogin(context, contact, false); } else { context.read().markContactRead(contact.publicKeyHex); Navigator.push( @@ -403,17 +438,13 @@ class _ContactsScreenState extends State case 1: Navigator.pushReplacement( context, - buildQuickSwitchRoute( - const ChannelsScreen(hideBackButton: true), - ), + buildQuickSwitchRoute(const ChannelsScreen(hideBackButton: true)), ); break; case 2: Navigator.pushReplacement( context, - buildQuickSwitchRoute( - const MapScreen(hideBackButton: true), - ), + buildQuickSwitchRoute(const MapScreen(hideBackButton: true)), ); break; } @@ -429,10 +460,8 @@ class _ContactsScreenState extends State Navigator.push( context, MaterialPageRoute( - builder: (context) => RepeaterHubScreen( - repeater: repeater, - password: password, - ), + builder: (context) => + RepeaterHubScreen(repeater: repeater, password: password), ), ); }, @@ -440,7 +469,7 @@ class _ContactsScreenState extends State ); } - void _showRoomLogin(BuildContext context, Contact room) { + void _showRoomLogin(BuildContext context, Contact room, bool settings) { showDialog( context: context, builder: (context) => RoomLoginDialog( @@ -451,7 +480,9 @@ class _ContactsScreenState extends State Navigator.push( context, MaterialPageRoute( - builder: (context) => ChatScreen(contact: room), + builder: (context) => settings + ? RepeaterHubScreen(repeater: room, password: password) + : ChatScreen(contact: room), ), ); }, @@ -459,7 +490,11 @@ class _ContactsScreenState extends State ); } - void _showGroupOptions(BuildContext context, ContactGroup group, List contacts) { + void _showGroupOptions( + BuildContext context, + ContactGroup group, + List contacts, + ) { final members = _resolveGroupContacts(group, contacts); showModalBottomSheet( context: context, @@ -478,7 +513,10 @@ class _ContactsScreenState extends State ), ListTile( leading: const Icon(Icons.delete, color: Colors.red), - title: Text(context.l10n.contacts_deleteGroup, style: const TextStyle(color: Colors.red)), + title: Text( + context.l10n.contacts_deleteGroup, + style: const TextStyle(color: Colors.red), + ), onTap: () { Navigator.pop(sheetContext); _confirmDeleteGroup(context, group); @@ -522,7 +560,10 @@ class _ContactsScreenState extends State }); await _saveGroups(); }, - child: Text(context.l10n.common_delete, style: const TextStyle(color: Colors.red)), + child: Text( + context.l10n.common_delete, + style: const TextStyle(color: Colors.red), + ), ), ], ), @@ -548,10 +589,16 @@ class _ContactsScreenState extends State final filteredContacts = filterQuery.isEmpty ? sortedContacts : sortedContacts - .where((contact) => matchesContactQuery(contact, filterQuery)) - .toList(); + .where( + (contact) => matchesContactQuery(contact, filterQuery), + ) + .toList(); return AlertDialog( - title: Text(isEditing ? context.l10n.contacts_editGroup : context.l10n.contacts_newGroup), + title: Text( + isEditing + ? context.l10n.contacts_editGroup + : context.l10n.contacts_newGroup, + ), content: SizedBox( width: double.maxFinite, child: Column( @@ -582,12 +629,18 @@ class _ContactsScreenState extends State SizedBox( height: 240, child: filteredContacts.isEmpty - ? Center(child: Text(context.l10n.contacts_noContactsMatchFilter)) + ? Center( + child: Text( + context.l10n.contacts_noContactsMatchFilter, + ), + ) : ListView.builder( itemCount: filteredContacts.length, itemBuilder: (context, index) { final contact = filteredContacts[index]; - final isSelected = selectedKeys.contains(contact.publicKeyHex); + final isSelected = selectedKeys.contains( + contact.publicKeyHex, + ); return CheckboxListTile( value: isSelected, title: Text(contact.name), @@ -618,7 +671,9 @@ class _ContactsScreenState extends State final name = nameController.text.trim(); if (name.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_groupNameRequired)), + SnackBar( + content: Text(context.l10n.contacts_groupNameRequired), + ), ); return; } @@ -628,13 +683,19 @@ class _ContactsScreenState extends State }); if (exists) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_groupAlreadyExists(name))), + SnackBar( + content: Text( + context.l10n.contacts_groupAlreadyExists(name), + ), + ), ); return; } setState(() { if (isEditing) { - final index = _groups.indexWhere((g) => g.name == group.name); + final index = _groups.indexWhere( + (g) => g.name == group.name, + ); if (index != -1) { _groups[index] = ContactGroup( name: name, @@ -642,7 +703,12 @@ class _ContactsScreenState extends State ); } } else { - _groups.add(ContactGroup(name: name, memberKeys: selectedKeys.toList())); + _groups.add( + ContactGroup( + name: name, + memberKeys: selectedKeys.toList(), + ), + ); } }); await _saveGroups(); @@ -650,7 +716,11 @@ class _ContactsScreenState extends State Navigator.pop(dialogContext); } }, - child: Text(isEditing ? context.l10n.common_save : context.l10n.common_create), + child: Text( + isEditing + ? context.l10n.common_save + : context.l10n.common_create, + ), ), ], ); @@ -675,23 +745,31 @@ class _ContactsScreenState extends State children: [ if (isRepeater) ListTile( - leading: const Icon(Icons.cell_tower, color: Colors.orange), + leading: const Icon(Icons.settings, color: Colors.orange), title: Text(context.l10n.contacts_manageRepeater), onTap: () { Navigator.pop(sheetContext); _showRepeaterLogin(context, contact); }, ) - else if (isRoom) + else if (isRoom) ...[ ListTile( leading: const Icon(Icons.room, color: Colors.blue), title: Text(context.l10n.contacts_roomLogin), onTap: () { Navigator.pop(sheetContext); - _showRoomLogin(context, contact); + _showRoomLogin(context, contact, false); }, - ) - else + ), + ListTile( + leading: const Icon(Icons.settings, color: Colors.orange), + title: Text(context.l10n.room_management), + onTap: () { + Navigator.pop(sheetContext); + _showRoomLogin(context, contact, true); + }, + ), + ] else ListTile( leading: const Icon(Icons.chat), title: Text(context.l10n.contacts_openChat), @@ -702,7 +780,10 @@ class _ContactsScreenState extends State ), ListTile( leading: const Icon(Icons.delete, color: Colors.red), - title: Text(context.l10n.contacts_deleteContact, style: const TextStyle(color: Colors.red)), + title: Text( + context.l10n.contacts_deleteContact, + style: const TextStyle(color: Colors.red), + ), onTap: () { Navigator.pop(sheetContext); _confirmDelete(context, connector, contact); @@ -734,7 +815,10 @@ class _ContactsScreenState extends State Navigator.pop(dialogContext); connector.removeContact(contact); }, - child: Text(context.l10n.common_delete, style: const TextStyle(color: Colors.red)), + child: Text( + context.l10n.common_delete, + style: const TextStyle(color: Colors.red), + ), ), ], ), @@ -759,14 +843,17 @@ class _ContactTile extends StatelessWidget { @override Widget build(BuildContext context) { - final shotPublicKey = "<${contact.publicKeyHex.substring(0, 8)}...${contact.publicKeyHex.substring(contact.publicKeyHex.length - 8)}>"; + final shotPublicKey = + "<${contact.publicKeyHex.substring(0, 8)}...${contact.publicKeyHex.substring(contact.publicKeyHex.length - 8)}>"; return ListTile( leading: CircleAvatar( backgroundColor: _getTypeColor(contact.type), child: _buildContactAvatar(contact), ), title: Text(contact.name), - subtitle: Text('${contact.typeLabel} • ${contact.pathLabel} $shotPublicKey'), + subtitle: Text( + '${contact.typeLabel} • ${contact.pathLabel} $shotPublicKey', + ), trailing: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, @@ -791,10 +878,7 @@ class _ContactTile extends StatelessWidget { Widget _buildContactAvatar(Contact contact) { final emoji = firstEmoji(contact.name); if (emoji != null) { - return Text( - emoji, - style: const TextStyle(fontSize: 18), - ); + return Text(emoji, style: const TextStyle(fontSize: 18)); } return Icon(_getTypeIcon(contact.type), color: Colors.white, size: 20); } @@ -833,13 +917,19 @@ class _ContactTile extends StatelessWidget { 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.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); + 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); + return days == 1 + ? context.l10n.contacts_lastSeenDayAgo + : context.l10n.contacts_lastSeenDaysAgo(days); } } diff --git a/lib/screens/repeater_hub_screen.dart b/lib/screens/repeater_hub_screen.dart index b6e6d3f..5089f59 100644 --- a/lib/screens/repeater_hub_screen.dart +++ b/lib/screens/repeater_hub_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:meshcore_open/connector/meshcore_protocol.dart'; import '../l10n/l10n.dart'; import '../models/contact.dart'; import 'repeater_status_screen.dart'; @@ -25,10 +26,17 @@ class RepeaterHubScreen extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text(l10n.repeater_management), + Text( + repeater.type == advTypeRepeater + ? l10n.repeater_management + : l10n.room_management, + ), Text( repeater.name, - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.normal), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + ), ), ], ), @@ -39,135 +47,147 @@ class RepeaterHubScreen extends StatelessWidget { child: ListView( padding: const EdgeInsets.all(16), children: [ - // Repeater info card - Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - CircleAvatar( - radius: 40, - backgroundColor: Colors.orange, - child: const Icon(Icons.cell_tower, size: 40, color: Colors.white), + // Repeater info card + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + CircleAvatar( + radius: 40, + backgroundColor: Colors.orange, + child: const Icon( + Icons.cell_tower, + size: 40, + color: Colors.white, ), - const SizedBox(height: 16), - Text( - repeater.name, - style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + Text( + repeater.name, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, ), - const SizedBox(height: 8), - Text( - '<${repeater.publicKeyHex.substring(0, 8)}...${repeater.publicKeyHex.substring(repeater.publicKeyHex.length - 8)}>', - style: TextStyle(fontSize: 14, color: Colors.grey[600]), - ), - const SizedBox(height: 8), - Text( - repeater.pathLabel, - style: TextStyle(fontSize: 14, color: Colors.grey[600]), - ), - if (repeater.hasLocation) ...[ - const SizedBox(height: 4), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.location_on, size: 14, color: Colors.grey[600]), - const SizedBox(width: 4), - Text( - '${repeater.latitude?.toStringAsFixed(4)}, ${repeater.longitude?.toStringAsFixed(4)}', - style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + const SizedBox(height: 8), + Text( + '<${repeater.publicKeyHex.substring(0, 8)}...${repeater.publicKeyHex.substring(repeater.publicKeyHex.length - 8)}>', + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + ), + const SizedBox(height: 8), + Text( + repeater.pathLabel, + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + ), + if (repeater.hasLocation) ...[ + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.location_on, + size: 14, + color: Colors.grey[600], + ), + const SizedBox(width: 4), + Text( + '${repeater.latitude?.toStringAsFixed(4)}, ${repeater.longitude?.toStringAsFixed(4)}', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], ), - ], - ), - ], + ), + ], + ), ], - ), + ], ), ), - const SizedBox(height: 24), - Text( - l10n.repeater_managementTools, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 16), - // Status button - _buildManagementCard( - context, - icon: Icons.analytics, - title: l10n.repeater_status, - subtitle: l10n.repeater_statusSubtitle, - color: Colors.blue, - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => RepeaterStatusScreen( - repeater: repeater, - password: password, - ), + ), + const SizedBox(height: 24), + Text( + l10n.repeater_managementTools, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + // Status button + _buildManagementCard( + context, + icon: Icons.analytics, + title: l10n.repeater_status, + subtitle: l10n.repeater_statusSubtitle, + color: Colors.blue, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => RepeaterStatusScreen( + repeater: repeater, + password: password, ), - ); - }, - ), - const SizedBox(height: 16), - // Telemetry button - _buildManagementCard( - context, - icon: Icons.bar_chart_sharp, - title: l10n.repeater_telemetry, - subtitle: l10n.repeater_telemetrySubtitle, - color: Colors.teal, - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => TelemetryScreen( - repeater: repeater, - password: password, - ), + ), + ); + }, + ), + const SizedBox(height: 16), + // Telemetry button + _buildManagementCard( + context, + icon: Icons.bar_chart_sharp, + title: l10n.repeater_telemetry, + subtitle: l10n.repeater_telemetrySubtitle, + color: Colors.teal, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + TelemetryScreen(repeater: repeater, password: password), + ), + ); + }, + ), + const SizedBox(height: 12), + // CLI button + _buildManagementCard( + context, + icon: Icons.terminal, + title: l10n.repeater_cli, + subtitle: l10n.repeater_cliSubtitle, + color: Colors.green, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => RepeaterCliScreen( + repeater: repeater, + password: password, ), - ); - }, - ), - const SizedBox(height: 12), - // CLI button - _buildManagementCard( - context, - icon: Icons.terminal, - title: l10n.repeater_cli, - subtitle: l10n.repeater_cliSubtitle, - color: Colors.green, - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => RepeaterCliScreen( - repeater: repeater, - password: password, - ), + ), + ); + }, + ), + const SizedBox(height: 12), + // Settings button + _buildManagementCard( + context, + icon: Icons.settings, + title: l10n.repeater_settings, + subtitle: l10n.repeater_settingsSubtitle, + color: Colors.orange, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => RepeaterSettingsScreen( + repeater: repeater, + password: password, ), - ); - }, - ), - const SizedBox(height: 12), - // Settings button - _buildManagementCard( - context, - icon: Icons.settings, - title: l10n.repeater_settings, - subtitle: l10n.repeater_settingsSubtitle, - color: Colors.orange, - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => RepeaterSettingsScreen( - repeater: repeater, - password: password, - ), - ), - ); - }, - ), + ), + ); + }, + ), ], ), ), @@ -214,10 +234,7 @@ class RepeaterHubScreen extends StatelessWidget { const SizedBox(height: 4), Text( subtitle, - style: TextStyle( - fontSize: 14, - color: Colors.grey[600], - ), + style: TextStyle(fontSize: 14, color: Colors.grey[600]), ), ], ), From 2becbb342cbd80daf7d8f5750b9d19ec2c952e89 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Mon, 19 Jan 2026 16:55:39 -0800 Subject: [PATCH 06/11] Added buildGetCustomVarsFrame And added update to refreshDeviceInfo and _requestDeviceInfo. Added parsing of Custom Vars --- lib/connector/meshcore_connector.dart | 34 +++++++++++++++++++++++++++ lib/connector/meshcore_protocol.dart | 7 ++++++ 2 files changed, 41 insertions(+) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 96fb229..15614b5 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -110,6 +110,7 @@ class MeshCoreConnector extends ChangeNotifier { int _queueSyncRetries = 0; static const int _maxQueueSyncRetries = 3; static const int _queueSyncTimeoutMs = 5000; // 5 second timeout + Map? _currentCustomVars; // Channel syncing state (sequential pattern) bool _isSyncingChannels = false; @@ -196,6 +197,7 @@ class MeshCoreConnector extends ChangeNotifier { int? get currentBwHz => _currentBwHz; int? get currentSf => _currentSf; int? get currentCr => _currentCr; + Map? get currentCustomVars => _currentCustomVars; int? get batteryMillivolts => _batteryMillivolts; int get maxContacts => _maxContacts; int get maxChannels => _maxChannels; @@ -952,6 +954,7 @@ class MeshCoreConnector extends ChangeNotifier { await sendFrame(buildAppStartFrame()); await requestBatteryStatus(force: true); await sendFrame(buildGetRadioSettingsFrame()); + await sendFrame(buildGetCustomVarsFrame()); _scheduleSelfInfoRetry(); } @@ -959,6 +962,7 @@ class MeshCoreConnector extends ChangeNotifier { _awaitingSelfInfo = true; await sendFrame(buildDeviceQueryFrame()); await sendFrame(buildAppStartFrame()); + await sendFrame(buildGetCustomVarsFrame()); await requestBatteryStatus(); _scheduleSelfInfoRetry(); @@ -1696,6 +1700,8 @@ class MeshCoreConnector extends ChangeNotifier { case respCodeBattAndStorage: _handleBatteryAndStorage(frame); break; + case respCodeCustomVars: + _handleCustomVars(frame); default: debugPrint('Unknown frame code: $code'); } @@ -3107,6 +3113,34 @@ class MeshCoreConnector extends ChangeNotifier { _scheduleReconnect(); } + Map _parseKeyValueString(String input) { + final result = {}; + + // Split on commas first – empty entries are ignored. + for (final pair in input.split(',')) { + final trimmedPair = pair.trim(); + if (trimmedPair.isEmpty) continue; + + // Each pair must contain exactly one ':'. + final separatorIndex = trimmedPair.indexOf(':'); + if (separatorIndex == -1) continue; // malformed, skip + + final key = trimmedPair.substring(0, separatorIndex).trim(); + final value = trimmedPair.substring(separatorIndex + 1).trim(); + + if (key.isNotEmpty) { + result[key] = value; + } + } + + return result; + } + + void _handleCustomVars(Uint8List frame) { + final buf = BufferReader(frame.sublist(1)); + _currentCustomVars = _parseKeyValueString(buf.readString()); + } + void _setState(MeshCoreConnectionState newState) { if (_state != newState) { _state = newState; diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 21c4392..8469d61 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -129,6 +129,7 @@ const int cmdGetChannel = 31; const int cmdSetChannel = 32; const int cmdGetRadioSettings = 57; const int cmdGetTelemetryReq = 39; +const int cmdGetCustomVar = 40; const int cmdSetCustomVar = 41; const int cmdSendBinaryReq = 50; @@ -164,6 +165,7 @@ const int respCodeContactMsgRecvV3 = 16; const int respCodeChannelMsgRecvV3 = 17; const int respCodeChannelInfo = 18; const int respCodeRadioSettings = 25; +const int respCodeCustomVars = 21; // Push codes (async from device) const int pushCodeAdvert = 0x80; @@ -603,6 +605,11 @@ Uint8List buildGetRadioSettingsFrame() { return Uint8List.fromList([cmdGetRadioSettings]); } +//Build CMD_GET_CUSTOM_VARS frame +Uint8List buildGetCustomVarsFrame() { + return Uint8List.fromList([cmdGetCustomVar]); +} + // Calculate LoRa airtime for a packet // Based on Semtech SX127x datasheet formula // Returns airtime in milliseconds From 98fc2d6e0ab68ae3c30a0f2da08a878c348a3b16 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Mon, 19 Jan 2026 16:56:32 -0800 Subject: [PATCH 07/11] Updated gps setting to follow state of companion. --- lib/l10n/app_bg.arb | 8 +- lib/l10n/app_de.arb | 6 +- lib/l10n/app_en.arb | 4 +- lib/l10n/app_es.arb | 4 +- lib/l10n/app_fr.arb | 6 +- lib/l10n/app_it.arb | 4 +- lib/l10n/app_localizations.dart | 4 +- lib/l10n/app_localizations_en.dart | 4 +- lib/l10n/app_nl.arb | 4 +- lib/l10n/app_pl.arb | 4 +- lib/l10n/app_pt.arb | 6 +- lib/l10n/app_sk.arb | 4 +- lib/l10n/app_sl.arb | 6 +- lib/l10n/app_sv.arb | 4 +- lib/l10n/app_zh.arb | 4 +- lib/screens/settings_screen.dart | 156 +++++++++++++++-------------- lib/widgets/elements_ui.dart | 59 +++++++++++ 17 files changed, 174 insertions(+), 113 deletions(-) create mode 100644 lib/widgets/elements_ui.dart diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index effbd6c..7a3216d 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1349,8 +1349,8 @@ "channels_scanQrCodeComingSoon": "Ще излезе скоро", "channels_enterHashtag": "Въведете хаштаг", "channels_hashtagHint": "напр. #отбор", - "settings_locationIntervalSec": "Интервал (Секунди)", - "settings_locationGPSEnableSubtitle": "Активирайте GPS, за автоматично изпращане на данни за местоположението (ако е поддържано).", - "settings_locationIntervalInvalid": "Интервалът трябва да бъде поне 60 секунди и по-малко от 86400 секунди.", - "settings_locationGPSEnable": "Активиране на GPS" + "settings_locationIntervalSec": "Интервал за GPS (Секунди)", + "settings_locationGPSEnable": "Активиране на GPS", + "settings_locationGPSEnableSubtitle": "Активирайте автоматичното актуализиране на местоположението чрез GPS.", + "settings_locationIntervalInvalid": "Интервалът трябва да бъде поне 60 секунди и по-малко от 86400 секунди." } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index dead738..a208393 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1350,7 +1350,7 @@ "channels_enterHashtag": "Gib Hashtag ein", "channels_hashtagHint": "z.B. #team", "settings_locationGPSEnable": "GPS aktivieren", - "settings_locationGPSEnableSubtitle": "Aktivieren Sie GPS, um Standortdaten automatisch zu senden (falls unterstützt).", - "settings_locationIntervalSec": "Zeitintervall (Sekunden)", - "settings_locationIntervalInvalid": "Der Zeitraum muss mindestens 60 Sekunden betragen und weniger als 86400 Sekunden sein." + "settings_locationGPSEnableSubtitle": "Aktivieren Sie die automatische Aktualisierung der Standortdaten per GPS.", + "settings_locationIntervalInvalid": "Der Zeitraum muss mindestens 60 Sekunden betragen und weniger als 86400 Sekunden sein.", + "settings_locationIntervalSec": "Zeitintervall für GPS (Sekunden)" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 9b2547c..988a876 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -87,8 +87,8 @@ "settings_locationBothRequired": "Enter both latitude and longitude.", "settings_locationInvalid": "Invalid latitude or longitude.", "settings_locationGPSEnable": "GPS Enable", - "settings_locationGPSEnableSubtitle": "Enable GPS to automatically send location data (if supported)", - "settings_locationIntervalSec": "Interval (Seconds)", + "settings_locationGPSEnableSubtitle": "Enables GPS to automatically update location.", + "settings_locationIntervalSec": "Interval for GPS (Seconds)", "settings_locationIntervalInvalid": "Interval must be at least 60 seconds, and less than 86400 seconds.", "settings_locationUpdated": "GPS settings updated.", "settings_latitude": "Latitude", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index bf90fe4..3c4453a 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1349,8 +1349,8 @@ "channels_scanQrCodeComingSoon": "Próximamente", "channels_enterHashtag": "Introducir hashtag", "channels_hashtagHint": "ej. #equipo", - "settings_locationGPSEnableSubtitle": "Habilitar GPS para enviar automáticamente datos de ubicación (si está disponible).", "settings_locationGPSEnable": "Habilitar GPS", - "settings_locationIntervalSec": "Intervalo (Segundos)", + "settings_locationGPSEnableSubtitle": "Habilita la actualización automática de la ubicación mediante GPS.", + "settings_locationIntervalSec": "Intervalo para GPS (Segundos)", "settings_locationIntervalInvalid": "El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 82d73d7..13cde8e 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1349,8 +1349,8 @@ "channels_scanQrCodeComingSoon": "Bientôt disponible", "channels_enterHashtag": "Entrez le hashtag", "channels_hashtagHint": "ex. #équipe", + "settings_locationGPSEnableSubtitle": "Activer la mise à jour automatique de la position grâce au GPS.", + "settings_locationIntervalInvalid": "L'intervalle doit être d’au moins 60 secondes et inférieur à 86400 secondes.", "settings_locationGPSEnable": "Activer le GPS", - "settings_locationGPSEnableSubtitle": "Activer le GPS pour envoyer automatiquement les données de localisation (si pris en charge).", - "settings_locationIntervalSec": "Intervalle (Secondes)", - "settings_locationIntervalInvalid": "L'intervalle doit être d'au moins 60 secondes et inférieur à 86400 secondes." + "settings_locationIntervalSec": "Intervalle GPS (Secondes)" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index f9828e5..d447c50 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1349,8 +1349,8 @@ "channels_scanQrCodeComingSoon": "Arriverà presto", "channels_enterHashtag": "Inserisci hashtag", "channels_hashtagHint": "es. #team", - "settings_locationGPSEnableSubtitle": "Abilita il GPS per inviare automaticamente i dati di posizione (se supportato).", "settings_locationGPSEnable": "Abilita GPS", - "settings_locationIntervalSec": "Intervallo (Secondi)", + "settings_locationGPSEnableSubtitle": "Abilita l'aggiornamento automatico della posizione tramite GPS.", + "settings_locationIntervalSec": "Intervallo GPS (Secondi)", "settings_locationIntervalInvalid": "L'intervallo deve essere di almeno 60 secondi e inferiore a 86400 secondi." } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index cfe77e2..2916b91 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -489,13 +489,13 @@ abstract class AppLocalizations { /// No description provided for @settings_locationGPSEnableSubtitle. /// /// In en, this message translates to: - /// **'Enable GPS to automatically send location data (if supported)'** + /// **'Enables GPS to automatically send location data (if supported)'** String get settings_locationGPSEnableSubtitle; /// No description provided for @settings_locationIntervalSec. /// /// In en, this message translates to: - /// **'Interval (Seconds)'** + /// **'Interval for GPS (Seconds)'** String get settings_locationIntervalSec; /// No description provided for @settings_locationIntervalInvalid. diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index e7ea0cf..a7e88cb 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -204,10 +204,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settings_locationGPSEnableSubtitle => - 'Enable GPS to automatically send location data (if supported)'; + 'Enables GPS to automatically send location data (if supported)'; @override - String get settings_locationIntervalSec => 'Interval (Seconds)'; + String get settings_locationIntervalSec => 'Interval for GPS (Seconds)'; @override String get settings_locationIntervalInvalid => diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index a0ddfe8..5f2e2f7 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1350,7 +1350,7 @@ "channels_enterHashtag": "Voer hashtag in", "channels_hashtagHint": "bijv. #team", "settings_locationGPSEnable": "GPS inschakelen", - "settings_locationGPSEnableSubtitle": "Zijze GPS inschakelen om locatiegegevens automatisch te verzenden (indien ondersteund).", + "settings_locationIntervalSec": "Interval voor GPS (Seconden)", "settings_locationIntervalInvalid": "De intervallen moeten minstens 60 seconden zijn en minder dan 86400 seconden.", - "settings_locationIntervalSec": "Interval (Seconden)" + "settings_locationGPSEnableSubtitle": "Activeer automatisch locatieupdates via GPS." } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 23938a5..15ce2b8 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1350,7 +1350,7 @@ "channels_enterHashtag": "Wprowadź hashtag", "channels_hashtagHint": "np. #zespół", "settings_locationGPSEnable": "Włącz GPS", - "settings_locationGPSEnableSubtitle": "Włącz GPS, aby automatycznie wysyłać dane o lokalizacji (jeśli jest obsługiwane).", - "settings_locationIntervalSec": "Interwał (Sekundy)", + "settings_locationIntervalSec": "Interwał dla GPS (Sekundy)", + "settings_locationGPSEnableSubtitle": "Włącza automatyczne aktualizowanie pozycji za pomocą GPS.", "settings_locationIntervalInvalid": "Interwał musi wynosić co najmniej 60 sekund i mniej niż 86400 sekund." } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index b49f286..0b09582 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1350,7 +1350,7 @@ "channels_enterHashtag": "Insira hashtag", "channels_hashtagHint": "ex. #equipe", "settings_locationGPSEnable": "Ativar GPS", - "settings_locationGPSEnableSubtitle": "Habilite o GPS para enviar dados de localização automaticamente (se suportado).", - "settings_locationIntervalSec": "Intervalo (Segundos)", - "settings_locationIntervalInvalid": "O intervalo deve ser de pelo menos 60 segundos e inferior a 86400 segundos." + "settings_locationGPSEnableSubtitle": "Habilita a atualização automática da localização via GPS.", + "settings_locationIntervalInvalid": "O intervalo deve ser de pelo menos 60 segundos e inferior a 86400 segundos.", + "settings_locationIntervalSec": "Intervalo para GPS (Segundos)" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index aa242c3..06de024 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1350,7 +1350,7 @@ "channels_enterHashtag": "Zadajte hashtag", "channels_hashtagHint": "napr. #tím", "settings_locationGPSEnable": "Aktivovať GPS", - "settings_locationGPSEnableSubtitle": "Zapnite GPS na automatické posielanie dát o polohe (ak je podporované).", - "settings_locationIntervalSec": "Interval (Sekundy)", + "settings_locationGPSEnableSubtitle": "Povolí automatické aktualizovanie polohy pomocou GPS.", + "settings_locationIntervalSec": "Interval pre GPS (Sekundy)", "settings_locationIntervalInvalid": "Interval musí byť aspoň 60 sekúnd a menej ako 86400 sekúnd." } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index e078d40..05e99b7 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1350,7 +1350,7 @@ "channels_enterHashtag": "Vnesite hashtag", "channels_hashtagHint": "npr. #ekipa", "settings_locationGPSEnable": "Omogoči GPS", - "settings_locationGPSEnableSubtitle": "Omogoči GPS za samodejno pošiljanje podatkov o lokaciji (če je podprto).", - "settings_locationIntervalSec": "Interval (Sekunde)", - "settings_locationIntervalInvalid": "Intervallo mora biti vsaj 60 sekund in manj kot 86400 sekund." + "settings_locationGPSEnableSubtitle": "Omogoči samodejno posodabljanje lokacije z GPS-jem.", + "settings_locationIntervalInvalid": "Intervallo mora biti vsaj 60 sekund in manj kot 86400 sekund.", + "settings_locationIntervalSec": "Interval za GPS (Sekunde)" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 2f580cb..deef6c6 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1349,8 +1349,8 @@ "channels_scanQrCodeComingSoon": "Kommer snart", "channels_enterHashtag": "Ange hashtag", "channels_hashtagHint": "t.ex. #team", + "settings_locationGPSEnableSubtitle": "Aktiverar automatiska uppdateringar av platsen med hjälp av GPS.", "settings_locationGPSEnable": "Aktivera GPS", - "settings_locationIntervalSec": "Tidsintervall (Sekunder)", - "settings_locationGPSEnableSubtitle": "Aktivera GPS för att automatiskt skicka platsdata (om det stöds).", + "settings_locationIntervalSec": "Interval för GPS (Sekunder)", "settings_locationIntervalInvalid": "Intervalet måste vara minst 60 sekunder och mindre än 86400 sekunder." } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index fa9e64d..d0f4dce 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1349,8 +1349,8 @@ "channels_scanQrCodeComingSoon": "即将到来", "channels_enterHashtag": "输入标签", "channels_hashtagHint": "例如 #团队", - "settings_locationGPSEnableSubtitle": "启用GPS自动发送位置数据(如果支持)。", "settings_locationGPSEnable": "启用GPS", - "settings_locationIntervalSec": "时间间隔(秒)", + "settings_locationGPSEnableSubtitle": "启用GPS自动更新位置。", + "settings_locationIntervalSec": "GPS 间隔(秒)", "settings_locationIntervalInvalid": "时间间隔必须至少为60秒,且小于86400秒。" } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index d925c30..c4ce483 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:meshcore_open/widgets/elements_ui.dart'; import 'package:provider/provider.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -435,7 +436,13 @@ class _SettingsScreenState extends State { intervalController.text = "900"; latController.text = connector.selfLatitude?.toStringAsFixed(6) ?? ''; lonController.text = connector.selfLongitude?.toStringAsFixed(6) ?? ''; - bool isGPSEnabled = false; + bool hasGPS = connector.currentCustomVars!.isNotEmpty + ? connector.currentCustomVars!.containsKey("gps") + : false; + + bool isGPSEnabled = hasGPS + ? connector.currentCustomVars!["gps"] == "1" + : false; showDialog( context: context, @@ -445,48 +452,30 @@ class _SettingsScreenState extends State { content: Column( mainAxisSize: MainAxisSize.min, children: [ - if (!isGPSEnabled) ...[ - TextField( - controller: latController, - decoration: InputDecoration( - labelText: l10n.settings_latitude, - border: const OutlineInputBorder(), - ), - keyboardType: const TextInputType.numberWithOptions( - decimal: true, - signed: true, - ), + TextField( + controller: latController, + decoration: InputDecoration( + labelText: l10n.settings_latitude, + border: const OutlineInputBorder(), ), - const SizedBox(height: 16), - TextField( - controller: lonController, - decoration: InputDecoration( - labelText: l10n.settings_longitude, - border: const OutlineInputBorder(), - ), - keyboardType: const TextInputType.numberWithOptions( - decimal: true, - signed: true, - ), - ), - ], - const SizedBox(height: 16), - CheckboxListTile( - value: isGPSEnabled, - enabled: true, - onChanged: (v) => - setDialogState(() => isGPSEnabled = v ?? false), - //controlAffinity: ListTileControlAffinity.leading, - title: Text( - l10n.settings_locationGPSEnable, - style: TextStyle(fontSize: 12), - ), - subtitle: Text( - l10n.settings_locationGPSEnableSubtitle, - style: TextStyle(fontSize: 10), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + signed: true, ), ), - if (isGPSEnabled) ...{ + const SizedBox(height: 16), + TextField( + controller: lonController, + decoration: InputDecoration( + labelText: l10n.settings_longitude, + border: const OutlineInputBorder(), + ), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + signed: true, + ), + ), + if (hasGPS) ...[ const SizedBox(height: 16), TextField( controller: intervalController, @@ -499,7 +488,21 @@ class _SettingsScreenState extends State { signed: false, ), ), - }, + const SizedBox(height: 16), + FeatureToggleRow( + title: l10n.settings_locationGPSEnable, + subtitle: l10n.settings_locationGPSEnableSubtitle, + value: isGPSEnabled, + onChanged: (value) async { + setDialogState(() => isGPSEnabled = value); + if (value) { + await connector.setCustomVar("gps:1"); + } else { + await connector.setCustomVar("gps:0"); + } + }, + ), + ], ], ), actions: [ @@ -510,11 +513,13 @@ class _SettingsScreenState extends State { TextButton( onPressed: () async { Navigator.pop(context); - if (isGPSEnabled) { + + if (hasGPS) { final intervalText = intervalController.text.trim(); if (intervalText.isEmpty) { return; } + final interval = int.tryParse(intervalText); if (interval == null || interval < 60) { if (!context.mounted) return; @@ -525,53 +530,50 @@ class _SettingsScreenState extends State { ); return; } - await connector.setCustomVar("gps:1"); + await connector.setCustomVar("gps_interval:$interval"); await connector.refreshDeviceInfo(); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(l10n.settings_locationUpdated)), ); - } else { - final latText = latController.text.trim(); - final lonText = lonController.text.trim(); - if (latText.isEmpty && lonText.isEmpty) { - return; - } + } - final currentLat = connector.selfLatitude; - final currentLon = connector.selfLongitude; - final lat = latText.isNotEmpty - ? double.tryParse(latText) - : currentLat; - final lon = lonText.isNotEmpty - ? double.tryParse(lonText) - : currentLon; - if (lat == null || lon == null) { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.settings_locationBothRequired), - ), - ); - return; - } - if (lat < -90 || lat > 90 || lon < -180 || lon > 180) { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_locationInvalid)), - ); - return; - } + final latText = latController.text.trim(); + final lonText = lonController.text.trim(); + if (latText.isEmpty && lonText.isEmpty) { + return; + } - await connector.setCustomVar("gps:0"); - await connector.setNodeLocation(lat: lat, lon: lon); - await connector.refreshDeviceInfo(); + final currentLat = connector.selfLatitude; + final currentLon = connector.selfLongitude; + final lat = latText.isNotEmpty + ? double.tryParse(latText) + : currentLat; + final lon = lonText.isNotEmpty + ? double.tryParse(lonText) + : currentLon; + if (lat == null || lon == null) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_locationUpdated)), + SnackBar(content: Text(l10n.settings_locationBothRequired)), ); + return; } + if (lat < -90 || lat > 90 || lon < -180 || lon > 180) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.settings_locationInvalid)), + ); + return; + } + + await connector.setNodeLocation(lat: lat, lon: lon); + await connector.refreshDeviceInfo(); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.settings_locationUpdated)), + ); }, child: Text(l10n.common_save), ), diff --git a/lib/widgets/elements_ui.dart b/lib/widgets/elements_ui.dart new file mode 100644 index 0000000..0c46249 --- /dev/null +++ b/lib/widgets/elements_ui.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +class FeatureToggleRow extends StatefulWidget { + final String title; + final String subtitle; + final bool value; + final bool hasRefreshing; + final bool isRefreshing; + final ValueChanged? onChanged; + final VoidCallback? onRefresh; + final String? refreshTooltip; + + const FeatureToggleRow({ + super.key, + required this.title, + required this.subtitle, + required this.value, + this.hasRefreshing = false, + this.isRefreshing = false, + this.onChanged, + this.onRefresh, + this.refreshTooltip, + }); + + @override + State createState() => _FeatureToggleRow(); +} + +class _FeatureToggleRow extends State { + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: SwitchListTile( + title: Text(widget.title), + subtitle: Text(widget.subtitle), + value: widget.value, + onChanged: widget.onChanged, + contentPadding: EdgeInsets.zero, + ), + ), + if (widget.hasRefreshing) + IconButton( + icon: widget.isRefreshing + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.refresh, size: 20), + onPressed: widget.isRefreshing ? null : widget.onRefresh, + tooltip: widget.refreshTooltip, + visualDensity: VisualDensity.compact, + ), + ], + ); + } +} From 30ba1799e1bfdf4ffdd3246e431c9f5a6f2dd638 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Mon, 19 Jan 2026 18:29:53 -0700 Subject: [PATCH 08/11] localization: update room management strings in multiple languages and refactor room login handling --- lib/l10n/app_fr.arb | 2 +- lib/l10n/app_localizations_bg.dart | 4 ++-- lib/l10n/app_localizations_de.dart | 4 ++-- lib/l10n/app_localizations_es.dart | 4 ++-- lib/l10n/app_localizations_fr.dart | 4 ++-- lib/l10n/app_localizations_it.dart | 4 ++-- lib/l10n/app_localizations_nl.dart | 4 ++-- lib/l10n/app_localizations_pl.dart | 4 ++-- lib/l10n/app_localizations_pt.dart | 4 ++-- lib/l10n/app_localizations_sk.dart | 4 ++-- lib/l10n/app_localizations_sl.dart | 4 ++-- lib/l10n/app_localizations_sv.dart | 4 ++-- lib/l10n/app_localizations_zh.dart | 4 ++-- lib/l10n/app_pt.arb | 2 +- lib/screens/contacts_screen.dart | 33 ++++++++++++++++++++---------- 15 files changed, 48 insertions(+), 37 deletions(-) diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 8d14b8f..6513b40 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1349,6 +1349,6 @@ "channels_scanQrCodeComingSoon": "Bientôt disponible", "channels_enterHashtag": "Entrez le hashtag", "channels_hashtagHint": "ex. #équipe", - "contacts_manageRoom": "Gérer le serveur de la pièce {name}", + "contacts_manageRoom": "Gérer le serveur de salle", "room_management": "Gestion du serveur de pièce" } diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 0c666fa..dcabe32 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -651,7 +651,7 @@ class AppLocalizationsBg extends AppLocalizations { String get contacts_manageRepeater => 'Управление на Повтарящ се Елемент'; @override - String get contacts_manageRoom => 'Manage Room Server'; + String get contacts_manageRoom => 'Управление на сървър за стая'; @override String get contacts_roomLogin => 'Вход в стаята'; @@ -1591,7 +1591,7 @@ class AppLocalizationsBg extends AppLocalizations { String get repeater_management => 'Управление на повторители'; @override - String get room_management => 'Room Server Management'; + String get room_management => 'Управление на сървъра за стая'; @override String get repeater_managementTools => 'Инструменти за управление'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index d3edf4e..c42ed40 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -648,7 +648,7 @@ class AppLocalizationsDe extends AppLocalizations { String get contacts_manageRepeater => 'Wiederholungen verwalten'; @override - String get contacts_manageRoom => 'Manage Room Server'; + String get contacts_manageRoom => 'Verwalten Sie den Raumserver'; @override String get contacts_roomLogin => 'Raum-Login'; @@ -1590,7 +1590,7 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_management => 'Repeater-Verwaltung'; @override - String get room_management => 'Room Server Management'; + String get room_management => 'Raumserververwaltung'; @override String get repeater_managementTools => 'Verwaltungs-Tools'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 9f92ba7..a88cccf 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -649,7 +649,7 @@ class AppLocalizationsEs extends AppLocalizations { String get contacts_manageRepeater => 'Gestionar Repetidor'; @override - String get contacts_manageRoom => 'Manage Room Server'; + String get contacts_manageRoom => 'Gestionar Servidor de Habitación'; @override String get contacts_roomLogin => 'Inicio de Sala'; @@ -1589,7 +1589,7 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_management => 'Gestión de Repetidores'; @override - String get room_management => 'Room Server Management'; + String get room_management => 'Administración del Servidor de Habitación'; @override String get repeater_managementTools => 'Herramientas de Gestión'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index ced92dc..ceb2be5 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -650,7 +650,7 @@ class AppLocalizationsFr extends AppLocalizations { String get contacts_manageRepeater => 'Gérer le répétiteur'; @override - String get contacts_manageRoom => 'Manage Room Server'; + String get contacts_manageRoom => 'Gérer le serveur de salle'; @override String get contacts_roomLogin => 'Connexion Salle'; @@ -1595,7 +1595,7 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_management => 'Gestion des répétiteurs'; @override - String get room_management => 'Room Server Management'; + String get room_management => 'Gestion du serveur de pièce'; @override String get repeater_managementTools => 'Outils de Gestion'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 1351305..50e50de 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -647,7 +647,7 @@ class AppLocalizationsIt extends AppLocalizations { String get contacts_manageRepeater => 'Gestisci Ripetitore'; @override - String get contacts_manageRoom => 'Manage Room Server'; + String get contacts_manageRoom => 'Gestisci Server Camera'; @override String get contacts_roomLogin => 'Login Camera'; @@ -1587,7 +1587,7 @@ class AppLocalizationsIt extends AppLocalizations { String get repeater_management => 'Gestione Ripetitori'; @override - String get room_management => 'Room Server Management'; + String get room_management => 'Gestione del Server di Camera'; @override String get repeater_managementTools => 'Strumenti di Gestione'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index baf4668..d928365 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -645,7 +645,7 @@ class AppLocalizationsNl extends AppLocalizations { String get contacts_manageRepeater => 'Beheer Repeater'; @override - String get contacts_manageRoom => 'Manage Room Server'; + String get contacts_manageRoom => 'Beheer Ruimte Server'; @override String get contacts_roomLogin => 'Ruimte Inloggen'; @@ -1582,7 +1582,7 @@ class AppLocalizationsNl extends AppLocalizations { String get repeater_management => 'Beheer Repeaters'; @override - String get room_management => 'Room Server Management'; + String get room_management => 'Beheer Server Kamer'; @override String get repeater_managementTools => 'Beheerinstrumenten'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 5051b83..435a589 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -650,7 +650,7 @@ class AppLocalizationsPl extends AppLocalizations { String get contacts_manageRepeater => 'Zarządzaj Powtórzami'; @override - String get contacts_manageRoom => 'Manage Room Server'; + String get contacts_manageRoom => 'Zarządzaj Serwerem Pokoju'; @override String get contacts_roomLogin => 'Logowanie do pokoju'; @@ -1591,7 +1591,7 @@ class AppLocalizationsPl extends AppLocalizations { String get repeater_management => 'Zarządzanie Powtórzami'; @override - String get room_management => 'Room Server Management'; + String get room_management => 'Zarządzanie Serwerem Pokoju'; @override String get repeater_managementTools => 'Narzędzia Zarządzania'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 96cb130..2cdd2f3 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -650,7 +650,7 @@ class AppLocalizationsPt extends AppLocalizations { String get contacts_manageRepeater => 'Gerenciar Repetidor'; @override - String get contacts_manageRoom => 'Manage Room Server'; + String get contacts_manageRoom => 'Gerenciar Servidor de Sala'; @override String get contacts_roomLogin => 'Login no Quarto'; @@ -1589,7 +1589,7 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_management => 'Gerenciamento de Repetidor'; @override - String get room_management => 'Room Server Management'; + String get room_management => 'Gerenciamento de Servidor de Sala'; @override String get repeater_managementTools => 'Ferramentas de Gerenciamento'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index cdb39c7..2dda317 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -643,7 +643,7 @@ class AppLocalizationsSk extends AppLocalizations { String get contacts_manageRepeater => 'Spravovať opakované zoznamy'; @override - String get contacts_manageRoom => 'Manage Room Server'; + String get contacts_manageRoom => 'Spravovať server miestnosti'; @override String get contacts_roomLogin => 'Prihlásenie do miestnosti'; @@ -1584,7 +1584,7 @@ class AppLocalizationsSk extends AppLocalizations { String get repeater_management => 'Správa opakérov'; @override - String get room_management => 'Room Server Management'; + String get room_management => 'Správa servera miestnosti'; @override String get repeater_managementTools => 'Nástroje na správu'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index a54282c..122741c 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -645,7 +645,7 @@ class AppLocalizationsSl extends AppLocalizations { String get contacts_manageRepeater => 'Upravljajte Ponovitve'; @override - String get contacts_manageRoom => 'Manage Room Server'; + String get contacts_manageRoom => 'Upravljajte strežnik sobe'; @override String get contacts_roomLogin => 'Vnos v sobo'; @@ -1584,7 +1584,7 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_management => 'Upravljanje ponovitve'; @override - String get room_management => 'Room Server Management'; + String get room_management => 'Upravljanje stremlišča'; @override String get repeater_managementTools => 'Upravne orodje'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 2b595a8..a1d8bd8 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -639,7 +639,7 @@ class AppLocalizationsSv extends AppLocalizations { String get contacts_manageRepeater => 'Hantera Upprepare'; @override - String get contacts_manageRoom => 'Manage Room Server'; + String get contacts_manageRoom => 'Hantera Rumserver'; @override String get contacts_roomLogin => 'Rum Inloggning'; @@ -1573,7 +1573,7 @@ class AppLocalizationsSv extends AppLocalizations { String get repeater_management => 'Återuppspelarens Hantering'; @override - String get room_management => 'Room Server Management'; + String get room_management => 'Rumserverhantering'; @override String get repeater_managementTools => 'Administrationsverktyg'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 2c0cddb..57b1a96 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -612,7 +612,7 @@ class AppLocalizationsZh extends AppLocalizations { String get contacts_manageRepeater => '管理重复项'; @override - String get contacts_manageRoom => 'Manage Room Server'; + String get contacts_manageRoom => '管理房间服务器'; @override String get contacts_roomLogin => '房间登录'; @@ -1517,7 +1517,7 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_management => '重复器管理'; @override - String get room_management => 'Room Server Management'; + String get room_management => '房间服务器管理'; @override String get repeater_managementTools => '管理工具'; diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 041b479..e05297d 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1349,6 +1349,6 @@ "channels_scanQrCodeComingSoon": "Em breve", "channels_enterHashtag": "Insira hashtag", "channels_hashtagHint": "ex. #equipe", - "contacts_manageRoom": "Gerenciar Servidor de Sala {name} ({count})", + "contacts_manageRoom": "Gerenciar Servidor de Sala", "room_management": "Gerenciamento de Servidor de Sala" } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 8d15cc3..e91cd94 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -27,6 +27,11 @@ import 'map_screen.dart'; import 'repeater_hub_screen.dart'; import 'settings_screen.dart'; +enum RoomLoginDestination { + chat, + management, +} + class ContactsScreen extends StatefulWidget { final bool hideBackButton; @@ -277,8 +282,9 @@ class _ContactsScreenState extends State if (group.name.toLowerCase().contains(query)) return true; for (final key in group.memberKeys) { final contact = contactsByKey[key]; - if (contact != null && matchesContactQuery(contact, query)) + if (contact != null && matchesContactQuery(contact, query)) { return true; + } } return false; }) @@ -422,7 +428,7 @@ class _ContactsScreenState extends State if (contact.type == advTypeRepeater) { _showRepeaterLogin(context, contact); } else if (contact.type == advTypeRoom) { - _showRoomLogin(context, contact, false); + _showRoomLogin(context, contact, RoomLoginDestination.chat); } else { context.read().markContactRead(contact.publicKeyHex); Navigator.push( @@ -469,18 +475,21 @@ class _ContactsScreenState extends State ); } - void _showRoomLogin(BuildContext context, Contact room, bool settings) { + void _showRoomLogin( + BuildContext context, + Contact room, + RoomLoginDestination destination, + ) { showDialog( context: context, builder: (context) => RoomLoginDialog( room: room, onLogin: (password) { - // Navigate to chat screen after successful login context.read().markContactRead(room.publicKeyHex); Navigator.push( context, MaterialPageRoute( - builder: (context) => settings + builder: (context) => destination == RoomLoginDestination.management ? RepeaterHubScreen(repeater: room, password: password) : ChatScreen(contact: room), ), @@ -745,7 +754,7 @@ class _ContactsScreenState extends State children: [ if (isRepeater) ListTile( - leading: const Icon(Icons.settings, color: Colors.orange), + leading: const Icon(Icons.cell_tower, color: Colors.orange), title: Text(context.l10n.contacts_manageRepeater), onTap: () { Navigator.pop(sheetContext); @@ -758,15 +767,15 @@ class _ContactsScreenState extends State title: Text(context.l10n.contacts_roomLogin), onTap: () { Navigator.pop(sheetContext); - _showRoomLogin(context, contact, false); + _showRoomLogin(context, contact, RoomLoginDestination.chat); }, ), ListTile( - leading: const Icon(Icons.settings, color: Colors.orange), + leading: const Icon(Icons.room_preferences, color: Colors.orange), title: Text(context.l10n.room_management), onTap: () { Navigator.pop(sheetContext); - _showRoomLogin(context, contact, true); + _showRoomLogin(context, contact, RoomLoginDestination.management); }, ), ] else @@ -917,10 +926,12 @@ class _ContactTile extends StatelessWidget { final now = DateTime.now(); final diff = now.difference(lastSeen); - if (diff.isNegative || diff.inMinutes < 5) + if (diff.isNegative || diff.inMinutes < 5) { return context.l10n.contacts_lastSeenNow; - if (diff.inMinutes < 60) + } + if (diff.inMinutes < 60) { return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes); + } if (diff.inHours < 24) { final hours = diff.inHours; return hours == 1 From 3fef594fe579ce373dfbe40bff904db12714500c Mon Sep 17 00:00:00 2001 From: zjs81 Date: Mon, 19 Jan 2026 18:56:06 -0700 Subject: [PATCH 09/11] localization: update GPS settings messages and improve handling of custom variables --- lib/l10n/app_en.arb | 3 +-- lib/screens/settings_screen.dart | 17 +++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 7224b22..cc47077 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -83,14 +83,13 @@ "settings_radioSettingsUpdated": "Radio settings updated", "settings_location": "Location", "settings_locationSubtitle": "GPS coordinates", - "settings_locationUpdated": "Location updated", + "settings_locationUpdated": "Location and GPS settings updated", "settings_locationBothRequired": "Enter both latitude and longitude.", "settings_locationInvalid": "Invalid latitude or longitude.", "settings_locationGPSEnable": "GPS Enable", "settings_locationGPSEnableSubtitle": "Enables GPS to automatically update location.", "settings_locationIntervalSec": "Interval for GPS (Seconds)", "settings_locationIntervalInvalid": "Interval must be at least 60 seconds, and less than 86400 seconds.", - "settings_locationUpdated": "GPS settings updated.", "settings_latitude": "Latitude", "settings_longitude": "Longitude", "settings_privacyMode": "Privacy Mode", diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index c4ce483..c6a85d7 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -433,16 +433,17 @@ class _SettingsScreenState extends State { final latController = TextEditingController(); final lonController = TextEditingController(); final intervalController = TextEditingController(); - intervalController.text = "900"; latController.text = connector.selfLatitude?.toStringAsFixed(6) ?? ''; lonController.text = connector.selfLongitude?.toStringAsFixed(6) ?? ''; - bool hasGPS = connector.currentCustomVars!.isNotEmpty - ? connector.currentCustomVars!.containsKey("gps") - : false; - bool isGPSEnabled = hasGPS - ? connector.currentCustomVars!["gps"] == "1" - : false; + // Safe access to custom vars - may be null before device responds + final customVars = connector.currentCustomVars ?? {}; + final bool hasGPS = customVars.containsKey("gps"); + bool isGPSEnabled = customVars["gps"] == "1"; + + // Read current interval or default to 900 (15 minutes) + final currentInterval = int.tryParse(customVars["gps_interval"] ?? "") ?? 900; + intervalController.text = currentInterval.toString(); showDialog( context: context, @@ -521,7 +522,7 @@ class _SettingsScreenState extends State { } final interval = int.tryParse(intervalText); - if (interval == null || interval < 60) { + if (interval == null || interval < 60 || interval >= 86400) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( From 9cb667fad08c68d26d30ff037f7396e739bf3d14 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Mon, 19 Jan 2026 19:00:24 -0700 Subject: [PATCH 10/11] localization: fix punctuation in GPS interval settings for Spanish and Portuguese --- lib/l10n/app_es.arb | 2 +- lib/l10n/app_pt.arb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 03ea4dd..571bc65 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1352,7 +1352,7 @@ "settings_locationGPSEnable": "Habilitar GPS", "settings_locationGPSEnableSubtitle": "Habilita la actualización automática de la ubicación mediante GPS.", "settings_locationIntervalSec": "Intervalo para GPS (Segundos)", - "settings_locationIntervalInvalid": "El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos." + "settings_locationIntervalInvalid": "El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.", "contacts_manageRoom": "Gestionar Servidor de Habitación", "room_management": "Administración del Servidor de Habitación" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 8b95769..6f6471f 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1352,7 +1352,7 @@ "settings_locationGPSEnable": "Ativar GPS", "settings_locationGPSEnableSubtitle": "Habilita a atualização automática da localização via GPS.", "settings_locationIntervalInvalid": "O intervalo deve ser de pelo menos 60 segundos e inferior a 86400 segundos.", - "settings_locationIntervalSec": "Intervalo para GPS (Segundos)" + "settings_locationIntervalSec": "Intervalo para GPS (Segundos)", "contacts_manageRoom": "Gerenciar Servidor de Sala", "room_management": "Gerenciamento de Servidor de Sala" } From 9a9f59e53f2fc56acaf3cb5be5afea926dac7f9f Mon Sep 17 00:00:00 2001 From: zjs81 Date: Mon, 19 Jan 2026 19:00:30 -0700 Subject: [PATCH 11/11] localization: update GPS settings messages for clarity and consistency across multiple languages --- lib/l10n/app_localizations.dart | 4 ++-- lib/l10n/app_localizations_bg.dart | 4 ++-- lib/l10n/app_localizations_de.dart | 12 ++++++------ lib/l10n/app_localizations_en.dart | 4 ++-- lib/l10n/app_localizations_es.dart | 4 ++-- lib/l10n/app_localizations_fr.dart | 12 ++++++------ lib/l10n/app_localizations_it.dart | 4 ++-- lib/l10n/app_localizations_nl.dart | 4 ++-- lib/l10n/app_localizations_pl.dart | 4 ++-- lib/l10n/app_localizations_pt.dart | 4 ++-- lib/l10n/app_localizations_sk.dart | 6 +++--- lib/l10n/app_localizations_sl.dart | 12 ++++++------ lib/l10n/app_localizations_sv.dart | 4 ++-- lib/l10n/app_localizations_zh.dart | 4 ++-- 14 files changed, 41 insertions(+), 41 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index fc0e8e7..95e6935 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -465,7 +465,7 @@ abstract class AppLocalizations { /// No description provided for @settings_locationUpdated. /// /// In en, this message translates to: - /// **'GPS settings updated.'** + /// **'Location and GPS settings updated'** String get settings_locationUpdated; /// No description provided for @settings_locationBothRequired. @@ -489,7 +489,7 @@ abstract class AppLocalizations { /// No description provided for @settings_locationGPSEnableSubtitle. /// /// In en, this message translates to: - /// **'Enables GPS to automatically send location data (if supported)'** + /// **'Enables GPS to automatically update location.'** String get settings_locationGPSEnableSubtitle; /// No description provided for @settings_locationIntervalSec. diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 5627eea..7cbb2d6 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -206,10 +206,10 @@ class AppLocalizationsBg extends AppLocalizations { @override String get settings_locationGPSEnableSubtitle => - 'Активирайте GPS, за автоматично изпращане на данни за местоположението (ако е поддържано).'; + 'Активирайте автоматичното актуализиране на местоположението чрез GPS.'; @override - String get settings_locationIntervalSec => 'Интервал (Секунди)'; + String get settings_locationIntervalSec => 'Интервал за GPS (Секунди)'; @override String get settings_locationIntervalInvalid => diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 09290c5..5107d23 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -201,18 +201,18 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_locationInvalid => 'Ungültige Breiten- oder Längengrade.'; @override - String get settings_locationGPSEnable => 'GPS aktivieren'; + String get settings_locationGPSEnable => 'GPS Enable'; @override String get settings_locationGPSEnableSubtitle => - 'Aktivieren Sie GPS, um Standortdaten automatisch zu senden (falls unterstützt).'; + 'Enables GPS to automatically update location.'; @override - String get settings_locationIntervalSec => 'Zeitintervall (Sekunden)'; + String get settings_locationIntervalSec => 'Interval for GPS (Seconds)'; @override String get settings_locationIntervalInvalid => - 'Der Zeitraum muss mindestens 60 Sekunden betragen und weniger als 86400 Sekunden sein.'; + 'Interval must be at least 60 seconds, and less than 86400 seconds.'; @override String get settings_latitude => 'Breitengrad'; @@ -662,7 +662,7 @@ class AppLocalizationsDe extends AppLocalizations { String get contacts_manageRepeater => 'Wiederholungen verwalten'; @override - String get contacts_manageRoom => 'Verwalten Sie den Raumserver'; + String get contacts_manageRoom => 'Manage Room Server'; @override String get contacts_roomLogin => 'Raum-Login'; @@ -1604,7 +1604,7 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_management => 'Repeater-Verwaltung'; @override - String get room_management => 'Raumserververwaltung'; + String get room_management => 'Room Server Management'; @override String get repeater_managementTools => 'Verwaltungs-Tools'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 6dcaebf..abb776a 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -190,7 +190,7 @@ class AppLocalizationsEn extends AppLocalizations { String get settings_locationSubtitle => 'GPS coordinates'; @override - String get settings_locationUpdated => 'GPS settings updated.'; + String get settings_locationUpdated => 'Location and GPS settings updated'; @override String get settings_locationBothRequired => @@ -204,7 +204,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settings_locationGPSEnableSubtitle => - 'Enables GPS to automatically send location data (if supported)'; + 'Enables GPS to automatically update location.'; @override String get settings_locationIntervalSec => 'Interval for GPS (Seconds)'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index e70fc6a..8ab2c19 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -205,10 +205,10 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_locationGPSEnableSubtitle => - 'Habilitar GPS para enviar automáticamente datos de ubicación (si está disponible).'; + 'Habilita la actualización automática de la ubicación mediante GPS.'; @override - String get settings_locationIntervalSec => 'Intervalo (Segundos)'; + String get settings_locationIntervalSec => 'Intervalo para GPS (Segundos)'; @override String get settings_locationIntervalInvalid => diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 587043d..224f6a1 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -201,18 +201,18 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_locationInvalid => 'Latitude ou longitude invalide.'; @override - String get settings_locationGPSEnable => 'Activer le GPS'; + String get settings_locationGPSEnable => 'Habilita GPS'; @override String get settings_locationGPSEnableSubtitle => - 'Activer le GPS pour envoyer automatiquement les données de localisation (si pris en charge).'; + 'Habilita la actualización automática de la ubicación mediante GPS.'; @override - String get settings_locationIntervalSec => 'Intervalle (Secondes)'; + String get settings_locationIntervalSec => 'Intervalo pour GPS (Segundos)'; @override String get settings_locationIntervalInvalid => - 'L\'intervalle doit être d\'au moins 60 secondes et inférieur à 86400 secondes.'; + 'El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.'; @override String get settings_latitude => 'Latitude'; @@ -664,7 +664,7 @@ class AppLocalizationsFr extends AppLocalizations { String get contacts_manageRepeater => 'Gérer le répétiteur'; @override - String get contacts_manageRoom => 'Gérer le serveur de salle'; + String get contacts_manageRoom => 'Gestionar Servidor de Habitación'; @override String get contacts_roomLogin => 'Connexion Salle'; @@ -1609,7 +1609,7 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_management => 'Gestion des répétiteurs'; @override - String get room_management => 'Gestion du serveur de pièce'; + String get room_management => 'Administración del Servidor de Habitación'; @override String get repeater_managementTools => 'Outils de Gestion'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index c103eb5..b034425 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -205,10 +205,10 @@ class AppLocalizationsIt extends AppLocalizations { @override String get settings_locationGPSEnableSubtitle => - 'Abilita il GPS per inviare automaticamente i dati di posizione (se supportato).'; + 'Abilita l\'aggiornamento automatico della posizione tramite GPS.'; @override - String get settings_locationIntervalSec => 'Intervallo (Secondi)'; + String get settings_locationIntervalSec => 'Intervallo GPS (Secondi)'; @override String get settings_locationIntervalInvalid => diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 0b75744..a938b50 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -205,10 +205,10 @@ class AppLocalizationsNl extends AppLocalizations { @override String get settings_locationGPSEnableSubtitle => - 'Zijze GPS inschakelen om locatiegegevens automatisch te verzenden (indien ondersteund).'; + 'Activeer automatisch locatieupdates via GPS.'; @override - String get settings_locationIntervalSec => 'Interval (Seconden)'; + String get settings_locationIntervalSec => 'Interval voor GPS (Seconden)'; @override String get settings_locationIntervalInvalid => diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index c8a11f6..adb1f62 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -207,10 +207,10 @@ class AppLocalizationsPl extends AppLocalizations { @override String get settings_locationGPSEnableSubtitle => - 'Włącz GPS, aby automatycznie wysyłać dane o lokalizacji (jeśli jest obsługiwane).'; + 'Włącza automatyczne aktualizowanie pozycji za pomocą GPS.'; @override - String get settings_locationIntervalSec => 'Interwał (Sekundy)'; + String get settings_locationIntervalSec => 'Interwał dla GPS (Sekundy)'; @override String get settings_locationIntervalInvalid => diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 339a77c..6bea4b7 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -206,10 +206,10 @@ class AppLocalizationsPt extends AppLocalizations { @override String get settings_locationGPSEnableSubtitle => - 'Habilite o GPS para enviar dados de localização automaticamente (se suportado).'; + 'Habilita a atualização automática da localização via GPS.'; @override - String get settings_locationIntervalSec => 'Intervalo (Segundos)'; + String get settings_locationIntervalSec => 'Intervalo para GPS (Segundos)'; @override String get settings_locationIntervalInvalid => diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 20a7943..0c26184 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -205,10 +205,10 @@ class AppLocalizationsSk extends AppLocalizations { @override String get settings_locationGPSEnableSubtitle => - 'Zapnite GPS na automatické posielanie dát o polohe (ak je podporované).'; + 'Povolí automatické aktualizovanie polohy pomocou GPS.'; @override - String get settings_locationIntervalSec => 'Interval (Sekundy)'; + String get settings_locationIntervalSec => 'Interval pre GPS (Sekundy)'; @override String get settings_locationIntervalInvalid => @@ -1117,7 +1117,7 @@ class AppLocalizationsSk extends AppLocalizations { @override String get chat_clearPathSubtitle => - 'Znovu nájsť vynútene pri nasledujacej pošlite'; + 'Znovu nájsť vynútene pri nasledujúcej pošlite'; @override String get chat_pathCleared => diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 571ddcf..c33c9d6 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -205,10 +205,10 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_locationGPSEnableSubtitle => - 'Omogoči GPS za samodejno pošiljanje podatkov o lokaciji (če je podprto).'; + 'Omogoči samodejno posodabljanje lokacije z GPS-jem.'; @override - String get settings_locationIntervalSec => 'Interval (Sekunde)'; + String get settings_locationIntervalSec => 'Interval za GPS (Sekunde)'; @override String get settings_locationIntervalInvalid => @@ -449,7 +449,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get appSettings_enableNotificationsSubtitle => - 'Prejmujte obvestila o sporočilih in oglasih'; + 'Prejmite obvestila o sporočilih in oglasih'; @override String get appSettings_notificationPermissionDenied => @@ -645,7 +645,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get contacts_noContactsFound => - 'Niti ena osebe ali skupine ni najdena.'; + 'Niti ena oseba ali skupine ni najdena.'; @override String get contacts_deleteContact => 'Izbrisati Kontakt'; @@ -697,7 +697,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get contacts_noContactsMatchFilter => - 'Niti ena osebe ne ustreza vašemu kriteriju.'; + 'Niti ena oseba ne ustreza vašemu kriteriju.'; @override String get contacts_noMembers => 'Nič članov.'; @@ -1203,7 +1203,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get map_nodesNeedGps => - 'Omrežje morajo deliti svoje GPS koordinate,\nda se prikazajo na zemljeobrazniku.'; + 'Omrežje morajo deliti svoje GPS koordinate,\nda se prikazao na zemljeobrazniku.'; @override String map_nodesCount(int count) { diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 59b322a..d876dd9 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -204,10 +204,10 @@ class AppLocalizationsSv extends AppLocalizations { @override String get settings_locationGPSEnableSubtitle => - 'Aktivera GPS för att automatiskt skicka platsdata (om det stöds).'; + 'Aktivera automatiska uppdateringar av platsen med hjälp av GPS.'; @override - String get settings_locationIntervalSec => 'Tidsintervall (Sekunder)'; + String get settings_locationIntervalSec => 'Interval för GPS (Sekunder)'; @override String get settings_locationIntervalInvalid => diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 0a60655..f1dd506 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -200,10 +200,10 @@ class AppLocalizationsZh extends AppLocalizations { String get settings_locationGPSEnable => '启用GPS'; @override - String get settings_locationGPSEnableSubtitle => '启用GPS自动发送位置数据(如果支持)。'; + String get settings_locationGPSEnableSubtitle => '启用GPS自动更新位置。'; @override - String get settings_locationIntervalSec => '时间间隔(秒)'; + String get settings_locationIntervalSec => 'GPS 间隔(秒)'; @override String get settings_locationIntervalInvalid => '时间间隔必须至少为60秒,且小于86400秒。';