From 75610695c2fed89dd5688ef62d35d7418ff5a4c4 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 28 Feb 2026 19:11:11 -0800 Subject: [PATCH] Add contact settings and discovery features - Implemented contact settings in localization files for Swedish, Ukrainian, and Chinese. - Added new DiscoveryContact model to handle discovered contacts. - Created DiscoveryScreen to display discovered contacts with filtering and sorting options. - Integrated contact discovery storage to persist discovered contacts. - Updated settings screen to include options for automatic contact addition. - Enhanced app bar and list filter widgets for better user experience. - Fixed variable naming inconsistencies in contact model. --- lib/connector/meshcore_connector.dart | 119 ++++++-- lib/connector/meshcore_protocol.dart | 52 +++- lib/l10n/app_en.arb | 22 +- lib/l10n/app_localizations.dart | 108 +++++++ lib/l10n/app_localizations_bg.dart | 61 ++++ lib/l10n/app_localizations_de.dart | 61 ++++ lib/l10n/app_localizations_en.dart | 61 ++++ lib/l10n/app_localizations_es.dart | 61 ++++ lib/l10n/app_localizations_fr.dart | 61 ++++ lib/l10n/app_localizations_it.dart | 61 ++++ lib/l10n/app_localizations_nl.dart | 61 ++++ lib/l10n/app_localizations_pl.dart | 61 ++++ lib/l10n/app_localizations_pt.dart | 61 ++++ lib/l10n/app_localizations_ru.dart | 61 ++++ lib/l10n/app_localizations_sk.dart | 61 ++++ lib/l10n/app_localizations_sl.dart | 61 ++++ lib/l10n/app_localizations_sv.dart | 61 ++++ lib/l10n/app_localizations_uk.dart | 61 ++++ lib/l10n/app_localizations_zh.dart | 61 ++++ lib/models/contact.dart | 2 +- lib/models/discovery_contact.dart | 137 +++++++++ lib/screens/contacts_screen.dart | 16 ++ lib/screens/discovery_screen.dart | 347 +++++++++++++++++++++++ lib/screens/settings_screen.dart | 113 +++++++- lib/services/ble_debug_log_service.dart | 4 +- lib/storage/contact_discovery_store.dart | 59 ++++ lib/widgets/app_bar.dart | 15 +- lib/widgets/list_filter_widget.dart | 90 ++++++ 28 files changed, 1958 insertions(+), 41 deletions(-) create mode 100644 lib/models/discovery_contact.dart create mode 100644 lib/screens/discovery_screen.dart create mode 100644 lib/storage/contact_discovery_store.dart diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index ef19f02..c142f84 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:crypto/crypto.dart' as crypto; +import 'package:meshcore_open/models/discovery_contact.dart'; import 'package:pointycastle/export.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; @@ -24,6 +25,7 @@ import '../storage/channel_message_store.dart'; import '../storage/channel_order_store.dart'; import '../storage/channel_settings_store.dart'; import '../storage/channel_store.dart'; +import '../storage/contact_discovery_store.dart'; import '../storage/contact_settings_store.dart'; import '../storage/contact_store.dart'; import '../storage/message_store.dart'; @@ -111,6 +113,7 @@ class MeshCoreConnector extends ChangeNotifier { final List _scanResults = []; final List _contacts = []; + final List _discoveredContacts = []; final List _channels = []; final Map> _conversations = {}; final Map> _channelMessages = {}; @@ -155,6 +158,12 @@ class MeshCoreConnector extends ChangeNotifier { bool _batteryRequested = false; bool _awaitingSelfInfo = false; bool _preserveContactsOnRefresh = false; + bool _autoAddUsers = false; + bool _autoAddRepeaters = false; + bool _autoAddRoomServers = false; + bool _autoAddSensors = false; + bool _overwriteOldest = false; + static const int _defaultMaxContacts = 32; static const int _defaultMaxChannels = 8; int _maxContacts = _defaultMaxContacts; @@ -195,6 +204,7 @@ class MeshCoreConnector extends ChangeNotifier { final ChannelSettingsStore _channelSettingsStore = ChannelSettingsStore(); final ContactSettingsStore _contactSettingsStore = ContactSettingsStore(); final ContactStore _contactStore = ContactStore(); + final ContactDiscoveryStore _discoveryContactStore = ContactDiscoveryStore(); final ChannelStore _channelStore = ChannelStore(); final UnreadStore _unreadStore = UnreadStore(); List _cachedChannels = []; @@ -242,6 +252,10 @@ class MeshCoreConnector extends ChangeNotifier { ); } + List get discoveredContacts { + return List.unmodifiable(_discoveredContacts); + } + List get channels => List.unmodifiable(_channels); bool get isConnected => _state == MeshCoreConnectionState.connected; bool get isLoadingContacts => _isLoadingContacts; @@ -258,6 +272,11 @@ class MeshCoreConnector extends ChangeNotifier { int? get currentBwHz => _currentBwHz; int? get currentSf => _currentSf; int? get currentCr => _currentCr; + bool? get autoAddUsers => _autoAddUsers; + bool? get autoAddRepeaters => _autoAddRepeaters; + bool? get autoAddRoomServers => _autoAddRoomServers; + bool? get autoAddSensors => _autoAddSensors; + bool? get autoAddOverwriteOldest => _overwriteOldest; bool? get clientRepeat => _clientRepeat; int? get firmwareVerCode => _firmwareVerCode; Map? get currentCustomVars => _currentCustomVars; @@ -602,6 +621,13 @@ class MeshCoreConnector extends ChangeNotifier { } } + Future loadDiscoveredContactCache() async { + final cached = await _discoveryContactStore.loadContacts(); + _discoveredContacts + ..clear() + ..addAll(cached); + } + Future loadChannelSettings({int? maxChannels}) async { _channelSmazEnabled.clear(); final channelCount = maxChannels ?? _maxChannels; @@ -852,6 +878,9 @@ class MeshCoreConnector extends ChangeNotifier { // Fetch channels so we can track unread counts for incoming messages unawaited(getChannels()); + + // Load discovered contacts from storage + unawaited(loadDiscoveredContactCache()); } catch (e) { debugPrint("Connection error: $e"); await disconnect(manual: false); @@ -972,6 +1001,7 @@ class MeshCoreConnector extends ChangeNotifier { _deviceDisplayName = null; _deviceId = null; _contacts.clear(); + _discoveredContacts.clear(); _conversations.clear(); _loadedConversationKeys.clear(); _selfPublicKey = null; @@ -1064,6 +1094,7 @@ class MeshCoreConnector extends ChangeNotifier { await requestBatteryStatus(force: true); await sendFrame(buildGetRadioSettingsFrame()); await sendFrame(buildGetCustomVarsFrame()); + await sendFrame(buildGetAutoAddFlagsFrame()); _scheduleSelfInfoRetry(); } @@ -1074,7 +1105,7 @@ class MeshCoreConnector extends ChangeNotifier { await sendFrame(buildAppStartFrame()); await sendFrame(buildGetCustomVarsFrame()); await requestBatteryStatus(); - + await sendFrame(buildGetAutoAddFlagsFrame()); _scheduleSelfInfoRetry(); } @@ -1903,8 +1934,8 @@ class MeshCoreConnector extends ChangeNotifier { case respCodeChannelInfo: _handleChannelInfo(frame); break; - case respCodeRadioSettings: - _handleRadioSettings(frame); + case respCodeAutoAddConfig: + _handleAutoAddConfig(frame); break; case respCodeBattAndStorage: _handleBatteryAndStorage(frame); @@ -1985,6 +2016,10 @@ class MeshCoreConnector extends ChangeNotifier { _selfLatitude = readInt32LE(frame, 36) / 1000000.0; _selfLongitude = readInt32LE(frame, 40) / 1000000.0; + if (frame.length >= 47 && frame[47] == 0x00) { + sendFrame(buildSetOtherParamsFrame(0, 0, 0)); + } + // Radio settings (if frame is long enough) if (frame.length >= 58) { _currentFreqHz = readUint32LE(frame, 48); @@ -1992,7 +2027,6 @@ class MeshCoreConnector extends ChangeNotifier { _currentSf = frame[56]; _currentCr = frame[57]; } - // Node name starts at offset 58 if frame is long enough if (frame.length > 58) { _selfName = readCString(frame, 58, frame.length - 58); @@ -2056,25 +2090,6 @@ class MeshCoreConnector extends ChangeNotifier { unawaited(_requestNextQueuedMessage()); } - void _handleRadioSettings(Uint8List frame) { - // Frame format from C++: - // [0] = RESP_CODE_RADIO_SETTINGS - // [1-4] = freq (uint32 LE, in Hz) - // [5-8] = bw (uint32 LE, in Hz) - // [9] = sf - // [10] = cr - if (frame.length >= 11) { - _currentFreqHz = readUint32LE(frame, 1); - _currentBwHz = readUint32LE(frame, 5); - _currentSf = frame[9]; - _currentCr = frame[10]; - debugPrint( - 'Radio settings: freq=$_currentFreqHz bw=$_currentBwHz sf=$_currentSf cr=$_currentCr', - ); - notifyListeners(); - } - } - void _handleBatteryAndStorage(Uint8List frame) { // Frame format from C++: // [0] = RESP_CODE_BATT_AND_STORAGE @@ -2275,6 +2290,10 @@ class MeshCoreConnector extends ChangeNotifier { await _contactStore.saveContacts(_contacts); } + Future _persistDiscoveredContacts() async { + await _discoveryContactStore.saveContacts(_discoveredContacts); + } + int _latestContactLastmod() { if (_contacts.isEmpty) return 0; var latest = 0; @@ -3739,6 +3758,7 @@ class MeshCoreConnector extends ChangeNotifier { return; } + //We ignore our own adverts if (listEquals(publicKey, _selfPublicKey)) { return; } @@ -3759,7 +3779,14 @@ class MeshCoreConnector extends ChangeNotifier { longitude: longitude, lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), ); - _handleContactAdvert(newContact); + if ((_autoAddUsers && type == advTypeChat) || + (_autoAddRepeaters && type == advTypeRepeater) || + (_autoAddRoomServers && type == advTypeRoom) || + (_autoAddSensors && type == advTypeSensor)) { + _handleContactAdvert(newContact); + } else { + _handleDiscovery(newContact); + } _updateDirectRepeater(newContact, snr, path); return; } @@ -3847,6 +3874,50 @@ class MeshCoreConnector extends ChangeNotifier { } notifyListeners(); } + + void _handleAutoAddConfig(Uint8List frame) { + final reader = BufferReader(frame); + try { + reader.skipBytes(1); // Skip the response code byte + final flags = reader.readByte(); + _autoAddUsers = flags & autoAddChatFlag != 0; + _autoAddRepeaters = flags & autoAddRepeaterFlag != 0; + _autoAddRoomServers = flags & autoAddRoomServerFlag != 0; + _autoAddSensors = flags & autoAddSensorFlag != 0; + _overwriteOldest = flags & autoAddOverwriteOldestFlag != 0; + } catch (e) { + appLogger.error('Failed to parse auto-add config: $e', tag: 'Connector'); + } + } + + void _handleDiscovery(Contact contact) { + debugPrint('Discovered new contact: ${contact.name}'); + final disContact = DiscoveryContact( + publicKey: contact.publicKey, + name: contact.name, + type: contact.type, + pathLength: contact.pathLength, + path: contact.path, + latitude: contact.latitude, + longitude: contact.longitude, + lastSeen: contact.lastSeen, + ); + _discoveredContacts.add(disContact); + + unawaited(_persistDiscoveredContacts()); + + // Show notification for new contact (advertisement) + if (_appSettingsService != null) { + final settings = _appSettingsService!.settings; + if (settings.notificationsEnabled && settings.notifyOnNewAdvert) { + _notificationService.showAdvertNotification( + contactName: contact.name, + contactType: contact.typeLabel, + contactId: contact.publicKeyHex, + ); + } + } + } } const int _phRouteMask = 0x03; diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index d5ce9ee..36c13e2 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -167,6 +167,8 @@ const int cmdGetTelemetryReq = 39; const int cmdGetCustomVar = 40; const int cmdSetCustomVar = 41; const int cmdSendBinaryReq = 50; +const int cmdSetAutoAddConfig = 58; +const int cmdGetAutoAddConfig = 59; // Text message types const int txtTypePlain = 0; @@ -200,8 +202,8 @@ const int respCodeDeviceInfo = 13; const int respCodeContactMsgRecvV3 = 16; const int respCodeChannelMsgRecvV3 = 17; const int respCodeChannelInfo = 18; -const int respCodeRadioSettings = 25; const int respCodeCustomVars = 21; +const int respCodeAutoAddConfig = 25; // Push codes (async from device) const int pushCodeAdvert = 0x80; @@ -247,6 +249,18 @@ const int payloadTypeCONTROL = 0x0B; // a control/discovery packet const int payloadTypeRawCustom = 0x0F; // custom packet as raw bytes, for applications with custom encryption, payloads, etc +//auto-add flags +const int autoAddOverwriteOldestFlag = + 1 << 0; // 0x01 - overwrite oldest non-favourite when full +const int autoAddChatFlag = + 1 << 1; // 0x02 - auto-add Chat (Companion) (ADV_TYPE_CHAT) +const int autoAddRepeaterFlag = + 1 << 2; // 0x04 - auto-add Repeater (ADV_TYPE_REPEATER) +const int autoAddRoomServerFlag = + 1 << 3; // 0x08 - auto-add Room Server (ADV_TYPE_ROOM) +const int autoAddSensorFlag = + 1 << 4; // 0x10 - auto-add Sensor (ADV_TYPE_SENSOR) + // Sizes const int pubKeySize = 32; const int maxPathSize = 64; @@ -297,7 +311,7 @@ const int contactNameOffset = 100; const int contactTimestampOffset = 132; const int contactLatOffset = 136; const int contactLonOffset = 140; -const int contactLastmodOffset = 144; +const int contactLastModOffset = 144; const int contactFrameSize = 148; // Message frame offsets @@ -685,6 +699,10 @@ Uint8List buildGetCustomVarsFrame() { return Uint8List.fromList([cmdGetCustomVar]); } +Uint8List buildGetAutoAddFlagsFrame() { + return Uint8List.fromList([cmdGetAutoAddConfig]); +} + // Calculate LoRa airtime for a packet // Based on Semtech SX127x datasheet formula // Returns airtime in milliseconds @@ -826,20 +844,40 @@ Uint8List buildZeroHopContact(Uint8List pubKey) { } // Build CMD_SET_OTHER_PARAMS frame -// Format: [cmd][allowAutoAddContacts][allowTelemetryFlags][advertLocationPolicy][multiAcks] +// Format: [cmd][allowTelemetryFlags][advertLocationPolicy][multiAcks] Uint8List buildSetOtherParamsFrame( - bool allowAutoAddContacts, int allowTelemetryFlags, int advertLocationPolicy, int multiAcks, ) { final writer = BufferWriter(); writer.writeByte(cmdSetOtherParams); - writer.writeByte( - allowAutoAddContacts ? 0x00 : 0x01, - ); // Allow Auto Add Contacts + //Going forward the app will just set Auto Add Contacts to disabled, and use the filter flags + //Allow Auto Add Contacts use inverted logic (0x01 = disabled, 0x00 = enabled). + writer.writeByte(0x01); writer.writeByte(allowTelemetryFlags); // Allow Telemetry Flags writer.writeByte(advertLocationPolicy); // Advertisement Location Policy writer.writeByte(multiAcks); // Multi Acknowledgements return writer.toBytes(); } + +// Build CMD_SET_AUTO_ADD_CONFIG frame +// Format: [cmd][flags] +Uint8List buildSetAutoAddConfigFrame({ + required bool autoAddChat, + required bool autoAddRepeater, + required bool autoAddRoomServer, + required bool autoAddSensor, + required bool overwriteOldest, +}) { + final writer = BufferWriter(); + writer.writeByte(cmdSetAutoAddConfig); + int flags = 0; + if (autoAddChat) flags |= autoAddChatFlag; + if (autoAddRepeater) flags |= autoAddRepeaterFlag; + if (autoAddRoomServer) flags |= autoAddRoomServerFlag; + if (autoAddSensor) flags |= autoAddSensorFlag; + if (overwriteOldest) flags |= autoAddOverwriteOldestFlag; + writer.writeByte(flags); + return writer.toBytes(); +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 175c346..3f92d0e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -98,6 +98,8 @@ "settings_locationIntervalInvalid": "Interval must be at least 60 seconds, and less than 86400 seconds.", "settings_latitude": "Latitude", "settings_longitude": "Longitude", + "settings_contactSettings": "Contact Settings", + "settings_contactSettingsSubtitle": "Settings for how contacts are added.", "settings_privacyMode": "Privacy Mode", "settings_privacyModeSubtitle": "Hide name/location in advertisements", "settings_privacyModeToggle": "Toggle privacy mode to hide your name and location in advertisements.", @@ -1837,5 +1839,21 @@ "settings_gpxExportShareText": "Map data exported from meshcore-open", "settings_gpxExportShareSubject": "meshcore-open GPX map data export", "snrIndicator_nearByRepeaters": "Nearby Repeaters", - "snrIndicator_lastSeen": "Last seen" -} + "snrIndicator_lastSeen": "Last seen", + "contactsSettings_title": "Contacts settings", + "contactsSettings_autoAddTitle": "Automatic Discovery", + "contactsSettings_otherTitle": "Other contact related settings", + "contactsSettings_autoAddUsersTitle": "Auto-add users", + "contactsSettings_autoAddUsersSubtitle": "Allow the companion to automatically add discovered users.", + "contactsSettings_autoAddRepeatersTitle": "Auto-add repeaters", + "contactsSettings_autoAddRepeatersSubtitle": "Allow the companion to automatically add discovered repeaters.", + "contactsSettings_autoAddRoomServersTitle": "Auto-add room servers", + "contactsSettings_autoAddRoomServersSubtitle": "Allow the companion to automatically add discovered room servers.", + "contactsSettings_autoAddSensorsTitle": "Auto-add sensors", + "contactsSettings_autoAddSensorsSubtitle": "Allow the companion to automatically add discovered sensors.", + "contactsSettings_overwriteOldestTitle": "Overwrite Oldest", + "contactsSettings_overwriteOldestSubtitle": "When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.", + "discoveredContacts_Title": "Discovered Contacts", + "discoveredContacts_noMatching": "No matching contacts", + "discoveredContacts_searchHint": "Search discovered contacts" +} \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index ff2c726..333147f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -544,6 +544,18 @@ abstract class AppLocalizations { /// **'Longitude'** String get settings_longitude; + /// No description provided for @settings_contactSettings. + /// + /// In en, this message translates to: + /// **'Contact Settings'** + String get settings_contactSettings; + + /// No description provided for @settings_contactSettingsSubtitle. + /// + /// In en, this message translates to: + /// **'Settings for how contacts are added.'** + String get settings_contactSettingsSubtitle; + /// No description provided for @settings_privacyMode. /// /// In en, this message translates to: @@ -5380,6 +5392,102 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Last seen'** String get snrIndicator_lastSeen; + + /// No description provided for @contactsSettings_title. + /// + /// In en, this message translates to: + /// **'Contacts settings'** + String get contactsSettings_title; + + /// No description provided for @contactsSettings_autoAddTitle. + /// + /// In en, this message translates to: + /// **'Automatic Discovery'** + String get contactsSettings_autoAddTitle; + + /// No description provided for @contactsSettings_otherTitle. + /// + /// In en, this message translates to: + /// **'Other contact related settings'** + String get contactsSettings_otherTitle; + + /// No description provided for @contactsSettings_autoAddUsersTitle. + /// + /// In en, this message translates to: + /// **'Auto-add users'** + String get contactsSettings_autoAddUsersTitle; + + /// No description provided for @contactsSettings_autoAddUsersSubtitle. + /// + /// In en, this message translates to: + /// **'Allow the companion to automatically add discovered users.'** + String get contactsSettings_autoAddUsersSubtitle; + + /// No description provided for @contactsSettings_autoAddRepeatersTitle. + /// + /// In en, this message translates to: + /// **'Auto-add repeaters'** + String get contactsSettings_autoAddRepeatersTitle; + + /// No description provided for @contactsSettings_autoAddRepeatersSubtitle. + /// + /// In en, this message translates to: + /// **'Allow the companion to automatically add discovered repeaters.'** + String get contactsSettings_autoAddRepeatersSubtitle; + + /// No description provided for @contactsSettings_autoAddRoomServersTitle. + /// + /// In en, this message translates to: + /// **'Auto-add room servers'** + String get contactsSettings_autoAddRoomServersTitle; + + /// No description provided for @contactsSettings_autoAddRoomServersSubtitle. + /// + /// In en, this message translates to: + /// **'Allow the companion to automatically add discovered room servers.'** + String get contactsSettings_autoAddRoomServersSubtitle; + + /// No description provided for @contactsSettings_autoAddSensorsTitle. + /// + /// In en, this message translates to: + /// **'Auto-add sensors'** + String get contactsSettings_autoAddSensorsTitle; + + /// No description provided for @contactsSettings_autoAddSensorsSubtitle. + /// + /// In en, this message translates to: + /// **'Allow the companion to automatically add discovered sensors.'** + String get contactsSettings_autoAddSensorsSubtitle; + + /// No description provided for @contactsSettings_overwriteOldestTitle. + /// + /// In en, this message translates to: + /// **'Overwrite Oldest'** + String get contactsSettings_overwriteOldestTitle; + + /// No description provided for @contactsSettings_overwriteOldestSubtitle. + /// + /// In en, this message translates to: + /// **'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'** + String get contactsSettings_overwriteOldestSubtitle; + + /// No description provided for @discoveredContacts_Title. + /// + /// In en, this message translates to: + /// **'Discovered Contacts'** + String get discoveredContacts_Title; + + /// No description provided for @discoveredContacts_noMatching. + /// + /// In en, this message translates to: + /// **'No matching contacts'** + String get discoveredContacts_noMatching; + + /// No description provided for @discoveredContacts_searchHint. + /// + /// In en, this message translates to: + /// **'Search discovered contacts'** + String get discoveredContacts_searchHint; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index da7ddc9..c26b5b2 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -234,6 +234,13 @@ class AppLocalizationsBg extends AppLocalizations { @override String get settings_longitude => 'Дължина'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Режим на поверителност'; @@ -3112,4 +3119,58 @@ class AppLocalizationsBg extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Последно видян'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 228ffe7..6d36471 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -233,6 +233,13 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_longitude => 'Längengrad'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Privatsphäreeinstellung'; @@ -3121,4 +3128,58 @@ class AppLocalizationsDe extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Zuletzt gesehen'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 70b3393..c40b7df 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -232,6 +232,13 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settings_longitude => 'Longitude'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Privacy Mode'; @@ -3065,4 +3072,58 @@ class AppLocalizationsEn extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Last seen'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 876666b..203916b 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -233,6 +233,13 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_longitude => 'Longitud'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Modo Privacidad'; @@ -3113,4 +3120,58 @@ class AppLocalizationsEs extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Visto por última vez'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 0c11eac..7ae1b25 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -234,6 +234,13 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_longitude => 'Longitude'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Mode de confidentialité'; @@ -3135,4 +3142,58 @@ class AppLocalizationsFr extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Dernière fois vu'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 8a8fe71..be88ecb 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -233,6 +233,13 @@ class AppLocalizationsIt extends AppLocalizations { @override String get settings_longitude => 'Longitudine'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Modalità Privacy'; @@ -3116,4 +3123,58 @@ class AppLocalizationsIt extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Ultimo accesso'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 8b4eee5..ad1f624 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -233,6 +233,13 @@ class AppLocalizationsNl extends AppLocalizations { @override String get settings_longitude => 'Lengtegraad'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Privacy Mode'; @@ -3103,4 +3110,58 @@ class AppLocalizationsNl extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Laatst gezien'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index cff6010..a97599c 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -235,6 +235,13 @@ class AppLocalizationsPl extends AppLocalizations { @override String get settings_longitude => 'Długość'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Tryb Prywatny'; @@ -3116,4 +3123,58 @@ class AppLocalizationsPl extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Ostatnio widziany'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 831d47a..e1f89e2 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -234,6 +234,13 @@ class AppLocalizationsPt extends AppLocalizations { @override String get settings_longitude => 'Longitude'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Modo de Privacidade'; @@ -3111,4 +3118,58 @@ class AppLocalizationsPt extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Visto pela última vez'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 5c73e3e..b2f3718 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -232,6 +232,13 @@ class AppLocalizationsRu extends AppLocalizations { @override String get settings_longitude => 'Долгота'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Режим конфиденциальности'; @@ -3123,4 +3130,58 @@ class AppLocalizationsRu extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Последний раз видели'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index b4f28fb..773b338 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -233,6 +233,13 @@ class AppLocalizationsSk extends AppLocalizations { @override String get settings_longitude => 'Dĺžka'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Režim ochrany súkromia'; @@ -3098,4 +3105,58 @@ class AppLocalizationsSk extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Naposledy videný'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index e015e45..0f38b94 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -233,6 +233,13 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_longitude => 'Dolžina'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Zasebnost'; @@ -3103,4 +3110,58 @@ class AppLocalizationsSl extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Zadnjič videno'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 3a25c3c..4e67267 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -232,6 +232,13 @@ class AppLocalizationsSv extends AppLocalizations { @override String get settings_longitude => 'Längdgrad'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Privatläge'; @@ -3081,4 +3088,58 @@ class AppLocalizationsSv extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Senast sedd'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index cd820cf..40a52ca 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -232,6 +232,13 @@ class AppLocalizationsUk extends AppLocalizations { @override String get settings_longitude => 'Довгота'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Режим приватності'; @@ -3130,4 +3137,58 @@ class AppLocalizationsUk extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Останній раз бачили'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index b63f714..4aec80b 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -226,6 +226,13 @@ class AppLocalizationsZh extends AppLocalizations { @override String get settings_longitude => '经度'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => '隐私模式'; @@ -2890,4 +2897,58 @@ class AppLocalizationsZh extends AppLocalizations { @override String get snrIndicator_lastSeen => '最近访问'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 5e532e6..7d8e011 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -183,7 +183,7 @@ class Contact { ) : Uint8List(0); final name = readCString(data, contactNameOffset, maxNameSize); - final lastmod = readUint32LE(data, contactLastmodOffset); + final lastmod = readUint32LE(data, contactLastModOffset); double? lat, lon; final latRaw = readInt32LE(data, contactLatOffset); diff --git a/lib/models/discovery_contact.dart b/lib/models/discovery_contact.dart new file mode 100644 index 0000000..54c2937 --- /dev/null +++ b/lib/models/discovery_contact.dart @@ -0,0 +1,137 @@ +import 'dart:typed_data'; +import '../connector/meshcore_protocol.dart'; + +class DiscoveryContact { + final Uint8List publicKey; + final String name; + final int type; + final int pathLength; // -1 = flood, 0+ = direct hops (from device) + final Uint8List path; // Path bytes from device + final double? latitude; + final double? longitude; + final DateTime lastSeen; + + DiscoveryContact({ + required this.publicKey, + required this.name, + required this.type, + required this.pathLength, + required this.path, + this.latitude, + this.longitude, + required this.lastSeen, + }); + + String get publicKeyHex => pubKeyToHex(publicKey); + + String get typeLabel { + switch (type) { + case advTypeChat: + return 'Chat'; + case advTypeRepeater: + return 'Repeater'; + case advTypeRoom: + return 'Room'; + case advTypeSensor: + return 'Sensor'; + default: + return 'Unknown'; + } + } + + String get pathLabel { + if (pathLength < 0) return 'Flood'; + if (pathLength == 0) return 'Direct'; + return '$pathLength hops'; + } + + bool get hasLocation => latitude != null && longitude != null; + + DiscoveryContact copyWith({ + Uint8List? publicKey, + String? name, + int? type, + int? pathLength, + Uint8List? path, + double? latitude, + double? longitude, + DateTime? lastSeen, + }) { + return DiscoveryContact( + publicKey: publicKey ?? this.publicKey, + name: name ?? this.name, + type: type ?? this.type, + pathLength: pathLength ?? this.pathLength, + path: path ?? this.path, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + lastSeen: lastSeen ?? this.lastSeen, + ); + } + + String get pathIdList { + final pathBytes = path; + if (pathBytes.isEmpty) return ''; + final parts = []; + final groupSize = pathHashSize; + for (int i = 0; i < pathBytes.length; i += groupSize) { + final end = (i + groupSize) <= pathBytes.length + ? (i + groupSize) + : pathBytes.length; + final chunk = pathBytes.sublist(i, end); + parts.add( + chunk + .map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()) + .join(), + ); + } + return parts.join(','); + } + + String get shortPubKeyHex { + return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>"; + } + + Uint8List? get traceRouteBytes { + final pathBytes = path; + Uint8List? traceBytes; + + if (pathBytes.isEmpty) { + traceBytes = Uint8List(1); + traceBytes[0] = publicKey[0]; + return traceBytes; + } + + if (type == advTypeRepeater || type == advTypeRoom) { + final len = (pathBytes.length + pathBytes.length + 1); + traceBytes = Uint8List(len); + traceBytes[pathBytes.length] = publicKey[0]; + for (int i = 0; i < pathBytes.length; i++) { + traceBytes[i] = pathBytes[i]; + if (i < pathBytes.length) { + traceBytes[len - 1 - i] = pathBytes[i]; + } + } + } else { + if (pathBytes.length < 2) { + return pathBytes[0] == 0 ? null : pathBytes; + } + final len = (pathBytes.length + pathBytes.length - 1); + traceBytes = Uint8List(len); + for (int i = 0; i < pathBytes.length; i++) { + traceBytes[i] = pathBytes[i]; + if (i < pathBytes.length - 1) { + traceBytes[len - 1 - i] = pathBytes[i]; + } + } + } + return traceBytes; + } + + @override + bool operator ==(Object other) => + other is DiscoveryContact && publicKeyHex == other.publicKeyHex; + + @override + int get hashCode => publicKeyHex.hashCode; +} diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index eeecfb9..6e9f841 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -26,6 +26,7 @@ import '../widgets/room_login_dialog.dart'; import '../widgets/unread_badge.dart'; import 'channels_screen.dart'; import 'chat_screen.dart'; +import 'discovery_screen.dart'; import 'map_screen.dart'; import 'repeater_hub_screen.dart'; import 'settings_screen.dart'; @@ -318,6 +319,21 @@ class _ContactsScreenState extends State ), onTap: () => _disconnect(context, connector), ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.person_add_rounded), + const SizedBox(width: 8), + Text("Discovered Contacts"), + ], + ), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const DiscoveryScreen(), + ), + ), + ), PopupMenuItem( child: Row( children: [ diff --git a/lib/screens/discovery_screen.dart b/lib/screens/discovery_screen.dart new file mode 100644 index 0000000..b8f49fa --- /dev/null +++ b/lib/screens/discovery_screen.dart @@ -0,0 +1,347 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:meshcore_open/models/contact.dart'; +import 'package:provider/provider.dart'; + +import '../connector/meshcore_connector.dart'; +import '../connector/meshcore_protocol.dart'; +import '../l10n/l10n.dart'; +import '../models/discovery_contact.dart'; +import '../utils/contact_search.dart'; +import '../widgets/app_bar.dart'; +import '../widgets/list_filter_widget.dart'; + +enum DiscoverySortOption { lastSeen, name, type } + +class DiscoveryScreen extends StatefulWidget { + const DiscoveryScreen({super.key}); + + @override + State createState() => _DiscoveryScreenState(); +} + +class _DiscoveryScreenState extends State { + final TextEditingController _searchController = TextEditingController(); + String searchQuery = ''; + ContactSortOption sortOption = ContactSortOption.lastSeen; + bool showUnreadOnly = false; + ContactTypeFilter typeFilter = ContactTypeFilter.all; + DiscoverySortOption discoverySortOption = DiscoverySortOption.lastSeen; + Timer? _searchDebounce; + + @override + void dispose() { + _searchController.dispose(); + _searchDebounce?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final connector = context.watch(); + + final discoveredContacts = connector.discoveredContacts; + final filteredAndSorted = _filterAndSortContacts( + discoveredContacts, + connector, + ); + + return Scaffold( + appBar: AppBar( + title: AppBarTitle( + l10n.discoveredContacts_Title, + indicators: false, + subtitle: false, + ), + centerTitle: true, + ), + body: Column( + children: [ + _buildFilters(filteredAndSorted, connector), + Expanded( + child: discoveredContacts.isEmpty + ? Center(child: Text(l10n.contacts_noContacts)) + : filteredAndSorted.isEmpty + ? Center(child: Text(l10n.discoveredContacts_noMatching)) + : ListView.builder( + itemCount: filteredAndSorted.length, + itemBuilder: (context, index) { + final contact = filteredAndSorted[index]; + return ListTile( + leading: CircleAvatar( + backgroundColor: _getTypeColor(contact.type), + child: Icon( + _getTypeIcon(contact.type), + color: Colors.white, + size: 20, + ), + ), + title: Text( + contact.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text( + contact.shortPubKeyHex, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + trailing: Text( + _formatLastSeen(context, contact.lastSeen), + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + ); + }, + ), + ), + ], + ), + ); + } + + Widget _buildFilters(filteredAndSorted, connector) { + final l10n = context.l10n; + + String hintText = ""; + switch (typeFilter) { + case ContactTypeFilter.all: + hintText = context.l10n.contacts_searchContacts( + filteredAndSorted.length, + showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + ); + break; + case ContactTypeFilter.users: + hintText = context.l10n.contacts_searchUsers( + filteredAndSorted.length, + showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + ); + break; + case ContactTypeFilter.repeaters: + hintText = context.l10n.contacts_searchRepeaters( + filteredAndSorted.length, + showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + ); + break; + case ContactTypeFilter.rooms: + hintText = context.l10n.contacts_searchRoomServers( + filteredAndSorted.length, + showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + ); + break; + case ContactTypeFilter.favorites: + hintText = context.l10n.contacts_searchFavorites( + filteredAndSorted.length, + showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + ); + break; + } + + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: hintText, + prefixIcon: const Icon(Icons.search), + suffixIcon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (searchQuery.isNotEmpty) + IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + _searchController.clear(); + setState(() { + searchQuery = ''; + }); + }, + ), + _buildFilterButton(context, connector), + ], + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + onChanged: (value) { + _searchDebounce?.cancel(); + _searchDebounce = Timer(const Duration(milliseconds: 300), () { + if (!mounted) return; + setState(() { + searchQuery = value.toLowerCase(); + }); + }); + }, + ), + ), + ], + ); + } + + Widget _buildFilterButton(BuildContext context, MeshCoreConnector connector) { + return DiscoveryContactsFilterMenu( + sortOption: sortOption, + typeFilter: typeFilter, + onSortChanged: (value) { + setState(() { + sortOption = value; + }); + }, + onTypeFilterChanged: (value) { + setState(() { + typeFilter = value; + }); + }, + ); + } + + List _filterAndSortContacts( + List contacts, + MeshCoreConnector connector, + ) { + var filtered = contacts.where((contact) { + if (searchQuery.isEmpty) return true; + return matchesContactQuery( + Contact( + publicKey: contact.publicKey, + name: contact.name, + type: contact.type, + pathLength: contact.pathLength, + path: contact.path, + lastSeen: contact.lastSeen, + ), + searchQuery, + ); + }).toList(); + + // Filter out own node from the list + if (connector.selfPublicKey != null) { + final selfPubKeyHex = pubKeyToHex(connector.selfPublicKey!); + filtered = filtered.where((contact) { + return contact.publicKeyHex != selfPubKeyHex; + }).toList(); + } + + if (typeFilter != ContactTypeFilter.all) { + filtered = filtered.where(_matchesTypeFilter).toList(); + } + + if (showUnreadOnly) { + filtered = filtered.where((contact) { + return connector.getUnreadCountForContact( + Contact( + publicKey: contact.publicKey, + name: contact.name, + type: contact.type, + pathLength: contact.pathLength, + path: contact.path, + lastSeen: contact.lastSeen, + ), + ) > + 0; + }).toList(); + } + + switch (sortOption) { + case ContactSortOption.lastSeen: + filtered.sort( + (a, b) => _resolveLastSeen(b).compareTo(_resolveLastSeen(a)), + ); + break; + case ContactSortOption.name: + filtered.sort( + (a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()), + ); + break; + default: + break; + } + + return filtered; + } + + bool _matchesTypeFilter(DiscoveryContact contact) { + switch (typeFilter) { + case ContactTypeFilter.all: + return true; + case ContactTypeFilter.users: + return contact.type == advTypeChat; + case ContactTypeFilter.repeaters: + return contact.type == advTypeRepeater; + case ContactTypeFilter.rooms: + return contact.type == advTypeRoom; + default: + return false; + } + } + + DateTime _resolveLastSeen(DiscoveryContact contact) { + if (contact.type != advTypeChat) return contact.lastSeen; + return contact.lastSeen.isAfter(contact.lastSeen) + ? contact.lastSeen + : contact.lastSeen; + } + + IconData _getTypeIcon(int type) { + switch (type) { + case advTypeChat: + return Icons.chat; + case advTypeRepeater: + return Icons.cell_tower; + case advTypeRoom: + return Icons.group; + case advTypeSensor: + return Icons.sensors; + default: + return Icons.device_unknown; + } + } + + Color _getTypeColor(int type) { + switch (type) { + case advTypeChat: + return Colors.blue; + case advTypeRepeater: + return Colors.orange; + case advTypeRoom: + return Colors.purple; + case advTypeSensor: + return Colors.green; + default: + return Colors.grey; + } + } + + String _formatLastSeen(BuildContext context, DateTime lastSeen) { + final now = DateTime.now(); + final diff = now.difference(lastSeen); + + if (diff.isNegative || diff.inMinutes < 5) { + return context.l10n.contacts_lastSeenNow; + } + if (diff.inMinutes < 60) { + return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes); + } + if (diff.inHours < 24) { + final hours = diff.inHours; + return hours == 1 + ? context.l10n.contacts_lastSeenHourAgo + : context.l10n.contacts_lastSeenHoursAgo(hours); + } + final days = diff.inDays; + return days == 1 + ? context.l10n.contacts_lastSeenDayAgo + : context.l10n.contacts_lastSeenDaysAgo(days); + } +} diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index a198f99..fe893f8 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -9,6 +9,7 @@ import '../connector/meshcore_protocol.dart'; import '../l10n/l10n.dart'; import '../models/radio_settings.dart'; import '../widgets/adaptive_app_bar_title.dart'; +import '../widgets/app_bar.dart'; import 'app_settings_screen.dart'; import 'app_debug_log_screen.dart'; import 'ble_debug_log_screen.dart'; @@ -43,8 +44,11 @@ class _SettingsScreenState extends State { final l10n = context.l10n; return Scaffold( appBar: AppBar( - title: AdaptiveAppBarTitle(l10n.settings_title), - centerTitle: true, + title: AppBarTitle( + l10n.settings_title, + indicators: false, + subtitle: false, + ), ), body: SafeArea( top: false, @@ -274,6 +278,14 @@ class _SettingsScreenState extends State { onTap: () => _editLocation(context, connector), ), const Divider(height: 1), + ListTile( + leading: const Icon(Icons.group_add_outlined), + title: Text(l10n.settings_contactSettings), + subtitle: Text(l10n.settings_contactSettingsSubtitle), + trailing: const Icon(Icons.chevron_right), + onTap: () => _editAutoAddConfig(context, connector), + ), + const Divider(height: 1), ListTile( leading: const Icon(Icons.visibility_off_outlined), title: Text(l10n.settings_privacyMode), @@ -849,6 +861,103 @@ class _SettingsScreenState extends State { ), ); } + + void _editAutoAddConfig(BuildContext context, MeshCoreConnector connector) { + final l10n = context.l10n; + bool autoAddChat = false; + bool autoAddRepeater = false; + bool autoAddRoomServer = false; + bool autoAddSensor = false; + bool overwriteOldest = false; + + final connector = context.read(); + autoAddChat = connector.autoAddUsers ?? false; + autoAddRepeater = connector.autoAddRepeaters ?? false; + autoAddRoomServer = connector.autoAddRoomServers ?? false; + autoAddSensor = connector.autoAddSensors ?? false; + overwriteOldest = connector.autoAddOverwriteOldest ?? false; + + showDialog( + context: context, + builder: (dialogContext) => StatefulBuilder( + builder: (context, setDialogState) => AlertDialog( + title: Text(l10n.contactsSettings_autoAddTitle), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + FeatureToggleRow( + title: l10n.contactsSettings_autoAddUsersTitle, + subtitle: l10n.contactsSettings_autoAddUsersSubtitle, + value: autoAddChat, + onChanged: (value) { + setDialogState(() => autoAddChat = value); + }, + ), + SizedBox(height: 8), + FeatureToggleRow( + title: l10n.contactsSettings_autoAddRepeatersTitle, + subtitle: l10n.contactsSettings_autoAddRepeatersSubtitle, + value: autoAddRepeater, + onChanged: (value) { + setDialogState(() => autoAddRepeater = value); + }, + ), + SizedBox(height: 8), + FeatureToggleRow( + title: l10n.contactsSettings_autoAddRoomServersTitle, + subtitle: l10n.contactsSettings_autoAddRoomServersSubtitle, + value: autoAddRoomServer, + onChanged: (value) { + setDialogState(() => autoAddRoomServer = value); + }, + ), + SizedBox(height: 8), + FeatureToggleRow( + title: l10n.contactsSettings_autoAddSensorsTitle, + subtitle: l10n.contactsSettings_autoAddSensorsSubtitle, + value: autoAddSensor, + onChanged: (value) { + setDialogState(() => autoAddSensor = value); + }, + ), + Divider(height: 4), + FeatureToggleRow( + title: l10n.contactsSettings_overwriteOldestTitle, + subtitle: l10n.contactsSettings_overwriteOldestSubtitle, + value: overwriteOldest, + onChanged: (value) { + setDialogState(() => overwriteOldest = value); + }, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(l10n.common_cancel), + ), + TextButton( + onPressed: () async { + final frame = buildSetAutoAddConfigFrame( + autoAddChat: autoAddChat, + autoAddRepeater: autoAddRepeater, + autoAddRoomServer: autoAddRoomServer, + autoAddSensor: autoAddSensor, + overwriteOldest: overwriteOldest, + ); + await connector.sendFrame(frame); + await connector.sendFrame(buildGetAutoAddFlagsFrame()); + Navigator.pop(context); + }, + child: Text(l10n.common_save), + ), + ], + ), + ), + ); + } } class _RadioSettingsDialog extends StatefulWidget { diff --git a/lib/services/ble_debug_log_service.dart b/lib/services/ble_debug_log_service.dart index bc46b59..d923d6b 100644 --- a/lib/services/ble_debug_log_service.dart +++ b/lib/services/ble_debug_log_service.dart @@ -215,8 +215,8 @@ class BleDebugLogService extends ChangeNotifier { return 'RESP_CODE_CHANNEL_MSG_RECV_V3'; case respCodeChannelInfo: return 'RESP_CODE_CHANNEL_INFO'; - case respCodeRadioSettings: - return 'RESP_CODE_RADIO_SETTINGS'; + case respCodeAutoAddConfig: + return 'RESP_CODE_AUTO_ADD_CONFIG'; case pushCodeTraceData: return 'PUSH_CODE_TRACE_DATA'; default: diff --git a/lib/storage/contact_discovery_store.dart b/lib/storage/contact_discovery_store.dart new file mode 100644 index 0000000..84f7807 --- /dev/null +++ b/lib/storage/contact_discovery_store.dart @@ -0,0 +1,59 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import '../models/discovery_contact.dart'; +import 'prefs_manager.dart'; + +class ContactDiscoveryStore { + static const String _key = 'discovered_contacts'; + + Future> loadContacts() async { + final prefs = PrefsManager.instance; + final jsonStr = prefs.getString(_key); + if (jsonStr == null) return []; + + try { + final jsonList = jsonDecode(jsonStr) as List; + return jsonList + .map((entry) => _fromJson(entry as Map)) + .toList(); + } catch (_) { + return []; + } + } + + Future saveContacts(List contacts) async { + final prefs = PrefsManager.instance; + final jsonList = contacts.map(_toJson).toList(); + await prefs.setString(_key, jsonEncode(jsonList)); + } + + Map _toJson(DiscoveryContact contact) { + return { + 'publicKey': base64Encode(contact.publicKey), + 'name': contact.name, + 'type': contact.type, + 'pathLength': contact.pathLength, + 'path': base64Encode(contact.path), + 'latitude': contact.latitude, + 'longitude': contact.longitude, + 'lastSeen': contact.lastSeen.millisecondsSinceEpoch, + }; + } + + DiscoveryContact _fromJson(Map json) { + final lastSeenMs = json['lastSeen'] as int? ?? 0; + return DiscoveryContact( + publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)), + name: json['name'] as String? ?? 'Unknown', + type: json['type'] as int? ?? 0, + pathLength: json['pathLength'] as int? ?? -1, + path: json['path'] != null + ? Uint8List.fromList(base64Decode(json['path'] as String)) + : Uint8List(0), + latitude: (json['latitude'] as num?)?.toDouble(), + longitude: (json['longitude'] as num?)?.toDouble(), + lastSeen: DateTime.fromMillisecondsSinceEpoch(lastSeenMs), + ); + } +} diff --git a/lib/widgets/app_bar.dart b/lib/widgets/app_bar.dart index e1cda77..7324481 100644 --- a/lib/widgets/app_bar.dart +++ b/lib/widgets/app_bar.dart @@ -9,7 +9,16 @@ class AppBarTitle extends StatelessWidget { final String title; final Widget? leading; final Widget? trailing; - const AppBarTitle(this.title, {this.leading, this.trailing, super.key}); + final bool indicators; + final bool subtitle; + const AppBarTitle( + this.title, { + this.leading, + this.trailing, + this.indicators = true, + this.subtitle = true, + super.key, + }); @override Widget build(BuildContext context) { @@ -23,10 +32,10 @@ class AppBarTitle extends StatelessWidget { : MediaQuery.sizeOf(context).width; final compact = availableWidth < 240; final showSubtitle = - !compact && connector.isConnected && selfName != null; + !compact && connector.isConnected && selfName != null && subtitle; final showBattery = availableWidth >= 60; final showSnr = availableWidth >= 110; - final showIndicators = showBattery || showSnr; + final showIndicators = (showBattery || showSnr) && indicators; return Row( mainAxisAlignment: MainAxisAlignment.start, diff --git a/lib/widgets/list_filter_widget.dart b/lib/widgets/list_filter_widget.dart index 473a3df..ee6fcd4 100644 --- a/lib/widgets/list_filter_widget.dart +++ b/lib/widgets/list_filter_widget.dart @@ -224,3 +224,93 @@ class ContactsFilterMenu extends StatelessWidget { ); } } + +class DiscoveryContactsFilterMenu extends StatelessWidget { + final ContactSortOption sortOption; + final ContactTypeFilter typeFilter; + final ValueChanged onSortChanged; + final ValueChanged onTypeFilterChanged; + + const DiscoveryContactsFilterMenu({ + super.key, + required this.sortOption, + required this.typeFilter, + required this.onSortChanged, + required this.onTypeFilterChanged, + }); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return SortFilterMenu( + tooltip: l10n.listFilter_tooltip, + sections: [ + SortFilterMenuSection( + title: l10n.listFilter_sortBy, + options: [ + SortFilterMenuOption( + value: _actionSortLastSeen, + label: l10n.listFilter_heardRecently, + checked: sortOption == ContactSortOption.lastSeen, + ), + SortFilterMenuOption( + value: _actionSortName, + label: l10n.listFilter_az, + checked: sortOption == ContactSortOption.name, + ), + ], + ), + SortFilterMenuSection( + title: l10n.listFilter_filters, + options: [ + SortFilterMenuOption( + value: _actionFilterAll, + label: l10n.listFilter_all, + checked: typeFilter == ContactTypeFilter.all, + ), + SortFilterMenuOption( + value: _actionFilterUsers, + label: l10n.listFilter_users, + checked: typeFilter == ContactTypeFilter.users, + ), + SortFilterMenuOption( + value: _actionFilterRepeaters, + label: l10n.listFilter_repeaters, + checked: typeFilter == ContactTypeFilter.repeaters, + ), + SortFilterMenuOption( + value: _actionFilterRooms, + label: l10n.listFilter_roomServers, + checked: typeFilter == ContactTypeFilter.rooms, + ), + ], + ), + ], + onSelected: (action) { + switch (action) { + case _actionSortName: + onSortChanged(ContactSortOption.name); + break; + case _actionSortLastSeen: + onSortChanged(ContactSortOption.lastSeen); + break; + case _actionFilterAll: + onTypeFilterChanged(ContactTypeFilter.all); + break; + case _actionFilterUsers: + onTypeFilterChanged(ContactTypeFilter.users); + break; + case _actionFilterFavorites: + onTypeFilterChanged(ContactTypeFilter.favorites); + break; + case _actionFilterRepeaters: + onTypeFilterChanged(ContactTypeFilter.repeaters); + break; + case _actionFilterRooms: + onTypeFilterChanged(ContactTypeFilter.rooms); + break; + } + }, + ); + } +}