Compare commits

...

24 Commits

Author SHA1 Message Date
Winston Lowe 5ad9263cc4 feat: Refactor repeater resolution logic across multiple screens 2026-03-19 16:17:25 -07:00
Winston Lowe 3f780ac667 feat: Enhance privacy settings and telemetry
- Implemented telemetry options for contacts, allowing users to enable or disable telemetry data sharing.
- Introduced a clear chat option in the chat interface for better message management.
- Updated the telemetry screen to handle telemetry data for contacts, including battery level.
- Refactored contact settings to include telemetry options and improved UI for better user experience.
2026-03-19 15:56:52 -07:00
zjs81 53caec3e14 Merge pull request #301 from just-stuff-tm/fix/tcp-flow-test-missing-provider
fix: provide AppSettingsService in tcp_flow_test
2026-03-16 16:10:29 -07:00
just_stuff_tm 3c440ca3d4 Merge branch 'zjs81:main' into fix/tcp-flow-test-missing-provider 2026-03-15 21:09:02 -04:00
zjs81 8797d8ffde Merge pull request #302 from stphnrdmr/doc/platform-support
Add more explicit platform support table
2026-03-15 15:21:22 -07:00
Stephan Rodemeier faba120823 Add more explicit platform support table
The platform support was a bit vague, this adds a table to better convey
the differences.
2026-03-15 23:01:38 +01:00
just-stuff-tm be690c8194 fix: provide AppSettingsService in tcp_flow_test
TcpScreen.initState reads AppSettingsService from context
to pre-fill host/port fields, but the test helper only
provided MeshCoreConnector. Switch to MultiProvider so
AppSettingsService is also in the widget tree.
2026-03-15 16:48:40 -04:00
zjs81 64d75dde45 chore: update version to 7.0.0+8 in pubspec.yaml 2026-03-14 18:46:29 -07:00
zjs81 9199aab7f7 Merge pull request #297 from zjs81/dev-improments
Improvements to path tracing and location handling
2026-03-14 18:42:58 -07:00
zjs81 60e8ee0130 fix: simplify method call for writing data in UsbSerialService 2026-03-14 18:41:57 -07:00
zjs81 6dfb7a4b69 fix: auto-add flag parsing, contact cache restore, and USB reconnect
- Fix operator precedence bug in _handleAutoAddConfig where `flags &
  flag != 0` was parsed as `flags & (flag != 0)`, always checking bit 0
  instead of the correct flag bit
- Populate _contacts from cache in loadContactCache() so contacts
  persist across app restarts
- Toggle DTR low→high on USB connect to force device to see a fresh
  connection
- Add 10ms inter-frame delay for USB sends to prevent missed responses
- Deassert DTR before closing USB port on disconnect/dispose

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 18:41:21 -07:00
zjs81 28a423e0a8 fix: correct location validation and clean up target contact handling
- Fix asymmetric lat/lon validation in _handleContactAdvert (was checking
  longitude != 0 for latitude; now uses (latitude != 0 || longitude != 0)
  for both)
- Remove duplicate targetGuessed assignment in path_trace_map
- Rename public target field to private _targetContact, use local variable
  to avoid unnecessary null-aware operators

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 18:14:39 -07:00
Winston Lowe 3593cfa843 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-14 18:10:44 -07:00
Winston Lowe dc85e7a41c Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-14 18:10:17 -07:00
Winston Lowe 9265daaf16 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-14 18:10:09 -07:00
Winston Lowe 4b744184c2 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-14 18:09:54 -07:00
zjs81 64698e0be6 Merge pull request #295 from ericszimmermann/ez_group_dropdown3
squashed PR for Dropdown Group Menu
2026-03-14 18:05:22 -07:00
zjs81 3dd9037be3 Merge remote-tracking branch 'origin/main' into ez_group_dropdown3
# Conflicts:
#	lib/main.dart
2026-03-14 18:02:31 -07:00
zjs81 566e3aadf8 fix: migrate filter menus to type-safe generics and harden popup dismissal
- Move ContactSortOption/ContactTypeFilter enums to dedicated
  contact_filter_types.dart (re-exported from contact_search.dart)
- Migrate ContactsFilterMenu and DiscoveryContactsFilterMenu to use
  sealed class action types with SortFilterMenu<T> generics, replacing
  int action constants and switch statements
- Guard _closeDropdownAndRun with ModalRoute.isCurrent check to prevent
  accidental dismissal of parent routes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 17:59:48 -07:00
Winston Lowe 06a906f4f7 Enhance location handling and improve path trace functionality across screens 2026-03-14 17:51:24 -07:00
zjs81 054a84031e Merge pull request #296 from zjs81/feature/ml-timeout-prediction
feat: add ML-based adaptive timeout prediction using LinearRegressor
2026-03-14 17:39:22 -07:00
ericz 86e9b7fe01 squashed commit of ez_group_dropdown 2026-03-15 00:34:09 +01:00
Winston Lowe 24fa78741b add TCP server address and port settings to AppSettings and update TcpScreen 2026-03-14 11:46:05 -07:00
Winston Lowe 79a45c527b Unify contact retrieval by introducing allContacts getter 2026-03-14 11:45:47 -07:00
70 changed files with 3567 additions and 795 deletions
+12 -5
View File
@@ -51,7 +51,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
### Device Management ### Device Management
- **BLE Connection**: Scan and connect to MeshCore devices via Bluetooth - **BLE, USB, TCP Connection**: Scan and connect to MeshCore devices via Bluetooth, USB or TCP
- **Device Settings**: Configure radio parameters, power settings, and network options - **Device Settings**: Configure radio parameters, power settings, and network options
- **Battery Monitoring**: Real-time battery status with chemistry-specific voltage curves - **Battery Monitoring**: Real-time battery status with chemistry-specific voltage curves
- **Firmware Updates**: Over-the-air firmware updates via BLE (coming soon) - **Firmware Updates**: Over-the-air firmware updates via BLE (coming soon)
@@ -75,10 +75,16 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
### Platform Support ### Platform Support
-**Android**: Full support (API 21+) | Feature | Android (API 21+) | iOS (12+) | Linux | Windows | macOS | Web |
-**iOS**: Full support (iOS 12+) |--------------------|:-----------------:|:---------:|:-----:|:-------:|:-----:|:---------------------------------:|
- 🚧 **Desktop**: Limited support (macOS/Linux/Windows) | BLE companion | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
- 🚧 **Web**: Under construction (Chrome) | USB companion | ✅ | 🚧 | ✅ | ✅ | ✅ | ✅ |
| TCP companion | ✅ | 🚧 | ✅ | ✅ | ✅ | ❌<br>(requires websocket bridge) |
| Core Functionality | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Mesh Network | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Map & Location | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Device Management | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Repeater Hub | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
### Dependencies ### Dependencies
@@ -189,6 +195,7 @@ Messages are transmitted as binary frames using a custom protocol optimized for
### App Settings ### App Settings
- **Theme**: System default, light, or dark mode - **Theme**: System default, light, or dark mode
- **Language**: Use one of 15 languages (English, Chinese, French, Spanish, Portuguese, German, Dutch, Polish, Swedish, Italian, Slovak, Slovene, Bulgarian, Russian, Ukrainian)
- **Notifications**: Configurable for messages, channels, and node advertisements - **Notifications**: Configurable for messages, channels, and node advertisements
- **Battery Chemistry**: Support for NMC, LiFePO4, and LiPo battery types - **Battery Chemistry**: Support for NMC, LiFePO4, and LiPo battery types
- **Message Retry**: Automatic retry with configurable path clearing - **Message Retry**: Automatic retry with configurable path clearing
+117 -13
View File
@@ -294,6 +294,10 @@ class MeshCoreConnector extends ChangeNotifier {
); );
} }
List<Contact> get allContacts => List.unmodifiable([
..._contacts,
..._discoveredContacts.where((c) => !c.isActive),
]);
List<Contact> get discoveredContacts { List<Contact> get discoveredContacts {
return List.unmodifiable(_discoveredContacts); return List.unmodifiable(_discoveredContacts);
} }
@@ -320,6 +324,11 @@ class MeshCoreConnector extends ChangeNotifier {
bool? get autoAddRoomServers => _autoAddRoomServers; bool? get autoAddRoomServers => _autoAddRoomServers;
bool? get autoAddSensors => _autoAddSensors; bool? get autoAddSensors => _autoAddSensors;
bool? get autoAddOverwriteOldest => _overwriteOldest; bool? get autoAddOverwriteOldest => _overwriteOldest;
int get telemetryModeBase => _telemetryModeBase;
int get telemetryModeLoc => _telemetryModeLoc;
int get telemetryModeEnv => _telemetryModeEnv;
int get advertLocationPolicy => _advertLocPolicy;
int get multiAcks => _multiAcks;
bool? get clientRepeat => _clientRepeat; bool? get clientRepeat => _clientRepeat;
int? get firmwareVerCode => _firmwareVerCode; int? get firmwareVerCode => _firmwareVerCode;
Map<String, String>? get currentCustomVars => _currentCustomVars; Map<String, String>? get currentCustomVars => _currentCustomVars;
@@ -726,6 +735,9 @@ class MeshCoreConnector extends ChangeNotifier {
_knownContactKeys _knownContactKeys
..clear() ..clear()
..addAll(cached.map((c) => c.publicKeyHex)); ..addAll(cached.map((c) => c.publicKeyHex));
_contacts
..clear()
..addAll(cached);
for (final contact in cached) { for (final contact in cached) {
_ensureContactSmazSettingLoaded(contact.publicKeyHex); _ensureContactSmazSettingLoaded(contact.publicKeyHex);
} }
@@ -1558,6 +1570,10 @@ class MeshCoreConnector extends ChangeNotifier {
if (_activeTransport == MeshCoreTransportType.usb) { if (_activeTransport == MeshCoreTransportType.usb) {
await _usbManager.write(data); await _usbManager.write(data);
// Brief pause so the device firmware can process each frame before the
// next arrives. Without this, rapid-fire frames over USB can cause the
// device to miss responses (especially on reconnect).
await Future<void>.delayed(const Duration(milliseconds: 10));
} else if (_activeTransport == MeshCoreTransportType.tcp) { } else if (_activeTransport == MeshCoreTransportType.tcp) {
await _tcpConnector.write(data); await _tcpConnector.write(data);
} else { } else {
@@ -1802,13 +1818,36 @@ class MeshCoreConnector extends ChangeNotifier {
} }
} }
Future<void> setContactFavorite(Contact contact, bool isFavorite) async { Future<void> setContactFlags(
Contact contact, {
bool? isFavorite,
bool? teleBase,
bool? teleLoc,
bool? teleEnv,
}) async {
if (!isConnected) return; if (!isConnected) return;
final latestContact = final latestContact =
await _fetchContactSnapshotFromDevice(contact.publicKey) ?? contact; await _fetchContactSnapshotFromDevice(contact.publicKey) ?? contact;
final updatedFlags = isFavorite int updatedFlags = isFavorite != null
? (latestContact.flags | contactFlagFavorite) ? (isFavorite
: (latestContact.flags & ~contactFlagFavorite); ? (latestContact.flags | contactFlagFavorite)
: (latestContact.flags & ~contactFlagFavorite))
: latestContact.flags;
updatedFlags = teleBase != null
? (teleBase
? (updatedFlags | contactFlagTeleBase)
: (updatedFlags & ~contactFlagTeleBase))
: updatedFlags;
updatedFlags = teleLoc != null
? (teleLoc
? (updatedFlags | contactFlagTeleLoc)
: (updatedFlags & ~contactFlagTeleLoc))
: updatedFlags;
updatedFlags = teleEnv != null
? (teleEnv
? (updatedFlags | contactFlagTeleEnv)
: (updatedFlags & ~contactFlagTeleEnv))
: updatedFlags;
await sendFrame( await sendFrame(
buildUpdateContactPathFrame( buildUpdateContactPathFrame(
@@ -2345,6 +2384,31 @@ class MeshCoreConnector extends ChangeNotifier {
await sendCliCommand('set privacy ${enabled ? 'on' : 'off'}'); await sendCliCommand('set privacy ${enabled ? 'on' : 'off'}');
} }
Future<void> setTelemetryModeBase(
int base,
int location,
int env,
int advert,
int multiAcks,
) async {
if (!isConnected) return;
_telemetryModeBase = base.clamp(teleModeDeny, teleModeAllowAll).toInt();
_telemetryModeLoc = location.clamp(teleModeDeny, teleModeAllowAll).toInt();
_telemetryModeEnv = env.clamp(teleModeDeny, teleModeAllowAll).toInt();
_advertLocPolicy = advert.clamp(0, 1).toInt();
_multiAcks = multiAcks.clamp(0, 2).toInt();
await sendFrame(
buildSetOtherParamsFrame(
(_telemetryModeEnv << 4) |
(_telemetryModeLoc << 2) |
_telemetryModeBase,
_advertLocPolicy,
_multiAcks,
),
);
notifyListeners();
}
Future<void> getChannels({int? maxChannels, bool force = false}) async { Future<void> getChannels({int? maxChannels, bool force = false}) async {
if (!isConnected) return; if (!isConnected) return;
if (_isSyncingChannels) { if (_isSyncingChannels) {
@@ -2962,6 +3026,8 @@ class MeshCoreConnector extends ChangeNotifier {
void _handleContact(Uint8List frame, {bool isContact = true}) { void _handleContact(Uint8List frame, {bool isContact = true}) {
final contact = Contact.fromFrame(frame); final contact = Contact.fromFrame(frame);
if (contact != null) { if (contact != null) {
_handleDiscovery(contact, frame, noNotify: true, addActive: true);
if (contact.type == advTypeRepeater) { if (contact.type == advTypeRepeater) {
_contactUnreadCount.remove(contact.publicKeyHex); _contactUnreadCount.remove(contact.publicKeyHex);
_unreadStore.saveContactUnreadCount( _unreadStore.saveContactUnreadCount(
@@ -4770,6 +4836,12 @@ class MeshCoreConnector extends ChangeNotifier {
(_autoAddRoomServers && type == advTypeRoom) || (_autoAddRoomServers && type == advTypeRoom) ||
(_autoAddSensors && type == advTypeSensor)) { (_autoAddSensors && type == advTypeSensor)) {
_handleContactAdvert(newContact); _handleContactAdvert(newContact);
_handleDiscovery(
newContact,
rawPacket,
noNotify: true,
addActive: true,
);
} else { } else {
_handleDiscovery(newContact, rawPacket); _handleDiscovery(newContact, rawPacket);
} }
@@ -4794,8 +4866,20 @@ class MeshCoreConnector extends ChangeNotifier {
// CRITICAL: Preserve user's path override when contact is refreshed from device // CRITICAL: Preserve user's path override when contact is refreshed from device
_contacts[existingIndex] = existing.copyWith( _contacts[existingIndex] = existing.copyWith(
latitude: hasLocation ? latitude : existing.latitude, latitude:
longitude: hasLocation ? longitude : existing.longitude, hasLocation &&
latitude != null &&
latitude.abs() <= 90 &&
(latitude != 0 || longitude != 0)
? latitude
: existing.latitude,
longitude:
hasLocation &&
longitude != null &&
longitude.abs() <= 180 &&
(latitude != 0 || longitude != 0)
? longitude
: existing.longitude,
name: hasName ? name : existing.name, name: hasName ? name : existing.name,
path: Uint8List.fromList(path.reversed.toList()), path: Uint8List.fromList(path.reversed.toList()),
pathLength: path.length, pathLength: path.length,
@@ -4866,11 +4950,11 @@ class MeshCoreConnector extends ChangeNotifier {
try { try {
reader.skipBytes(1); // Skip the response code byte reader.skipBytes(1); // Skip the response code byte
final flags = reader.readByte(); final flags = reader.readByte();
_autoAddUsers = flags & autoAddChatFlag != 0; _autoAddUsers = (flags & autoAddChatFlag) != 0;
_autoAddRepeaters = flags & autoAddRepeaterFlag != 0; _autoAddRepeaters = (flags & autoAddRepeaterFlag) != 0;
_autoAddRoomServers = flags & autoAddRoomServerFlag != 0; _autoAddRoomServers = (flags & autoAddRoomServerFlag) != 0;
_autoAddSensors = flags & autoAddSensorFlag != 0; _autoAddSensors = (flags & autoAddSensorFlag) != 0;
_overwriteOldest = flags & autoAddOverwriteOldestFlag != 0; _overwriteOldest = (flags & autoAddOverwriteOldestFlag) != 0;
} catch (e) { } catch (e) {
appLogger.error('Failed to parse auto-add config: $e', tag: 'Connector'); appLogger.error('Failed to parse auto-add config: $e', tag: 'Connector');
} }
@@ -4880,6 +4964,7 @@ class MeshCoreConnector extends ChangeNotifier {
Contact contact, Contact contact,
Uint8List rawPacket, { Uint8List rawPacket, {
bool noNotify = false, bool noNotify = false,
bool addActive = false,
}) { }) {
appLogger.info('Discovered new contact: ${contact.name}', tag: 'Connector'); appLogger.info('Discovered new contact: ${contact.name}', tag: 'Connector');
@@ -4900,7 +4985,7 @@ class MeshCoreConnector extends ChangeNotifier {
longitude: contact.longitude, longitude: contact.longitude,
lastSeen: contact.lastSeen, lastSeen: contact.lastSeen,
flags: 0, flags: 0,
isActive: false, isActive: addActive,
); );
notifyListeners(); notifyListeners();
unawaited(_persistDiscoveredContacts()); unawaited(_persistDiscoveredContacts());
@@ -4918,7 +5003,7 @@ class MeshCoreConnector extends ChangeNotifier {
longitude: contact.longitude, longitude: contact.longitude,
lastSeen: contact.lastSeen, lastSeen: contact.lastSeen,
lastMessageAt: contact.lastMessageAt, lastMessageAt: contact.lastMessageAt,
isActive: false, isActive: addActive,
flags: 0, flags: 0,
); );
_discoveredContacts.add(disContact); _discoveredContacts.add(disContact);
@@ -4943,6 +5028,25 @@ class MeshCoreConnector extends ChangeNotifier {
unawaited(_persistDiscoveredContacts()); unawaited(_persistDiscoveredContacts());
notifyListeners(); notifyListeners();
} }
void clearMessagesForContact(Contact contact) {
final contactKeyHex = contact.publicKeyHex;
final messages = _conversations[contactKeyHex];
if (messages == null) return;
messages.clear();
unawaited(_messageStore.saveMessages(contactKeyHex, messages));
markContactRead(contactKeyHex);
notifyListeners();
}
void clearMessagesForChannel(int channelIndex) {
final messages = _channelMessages[channelIndex];
if (messages == null) return;
messages.clear();
unawaited(_channelMessageStore.saveChannelMessages(channelIndex, messages));
markChannelRead(channelIndex);
notifyListeners();
}
} }
const int _phRouteMask = 0x03; const int _phRouteMask = 0x03;
@@ -64,6 +64,8 @@ class MeshCoreUsbManager {
Future<void> write(Uint8List data) => _service.write(data); Future<void> write(Uint8List data) => _service.write(data);
Future<void> writeRaw(Uint8List data) => _service.writeRaw(data);
// --- Label management --- // --- Label management ---
void updateConnectedLabel(String selfName) { void updateConnectedLabel(String selfName) {
_service.updateConnectedLabel(selfName); _service.updateConnectedLabel(selfName);
+23 -1
View File
@@ -210,7 +210,7 @@ const int cmdSetChannel = 32;
const int cmdSendTracePath = 36; const int cmdSendTracePath = 36;
const int cmdSetOtherParams = 38; const int cmdSetOtherParams = 38;
const int cmdSendAnonReq = 57; const int cmdSendAnonReq = 57;
const int cmdGetTelemetryReq = 39; const int cmdSendTelemetryReq = 39;
const int cmdGetCustomVar = 40; const int cmdGetCustomVar = 40;
const int cmdSetCustomVar = 41; const int cmdSetCustomVar = 41;
const int cmdSendBinaryReq = 50; const int cmdSendBinaryReq = 50;
@@ -272,6 +272,10 @@ const int advTypeRepeater = 2;
const int advTypeRoom = 3; const int advTypeRoom = 3;
const int advTypeSensor = 4; const int advTypeSensor = 4;
const int teleModeDeny = 0;
const int teleModeAllowFlags = 1; // use contact.flags
const int teleModeAllowAll = 2;
// Payload Types // Payload Types
const int payloadTypeREQ = const int payloadTypeREQ =
0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob) 0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
@@ -352,6 +356,9 @@ const int contactPubKeyOffset = 1;
const int contactTypeOffset = 33; const int contactTypeOffset = 33;
const int contactFlagsOffset = 34; const int contactFlagsOffset = 34;
const int contactFlagFavorite = 0x01; const int contactFlagFavorite = 0x01;
const int contactFlagTeleBase = 0x02; // 'base' permission includes battery
const int contactFlagTeleLoc = 0x04;
const int contactFlagTeleEnv = 0x08; //access environment sensors
const int contactPathLenOffset = 35; const int contactPathLenOffset = 35;
const int contactPathOffset = 36; const int contactPathOffset = 36;
const int contactNameOffset = 100; const int contactNameOffset = 100;
@@ -937,3 +944,18 @@ Uint8List buildSetAutoAddConfigFrame({
writer.writeByte(flags); writer.writeByte(flags);
return writer.toBytes(); return writer.toBytes();
} }
//Build CMD_SEND_TELEMETRY_REQ
// Format: [cmd][reserved x3][pub_key? x32]
Uint8List buildSendTelemetryReq(Uint8List? pubKey) {
final writer = BufferWriter();
writer.writeByte(cmdSendTelemetryReq);
if (pubKey != null && pubKey.length == pubKeySize) {
writer.writeBytes(Uint8List(3)); // reserved bytes
writer.writeBytes(pubKey);
} else {
writer.writeBytes(Uint8List(4)); // reserved bytes
}
return writer.toBytes();
}
+33 -1
View File
@@ -285,6 +285,7 @@
"contacts_newGroup": "Нова група", "contacts_newGroup": "Нова група",
"contacts_groupName": "Група", "contacts_groupName": "Група",
"contacts_groupNameRequired": "Името на групата е задължително.", "contacts_groupNameRequired": "Името на групата е задължително.",
"contacts_groupNameReserved": "Това име на група е запазено",
"contacts_groupAlreadyExists": "Групата \"{name}\" вече съществува.", "contacts_groupAlreadyExists": "Групата \"{name}\" вече съществува.",
"@contacts_groupAlreadyExists": { "@contacts_groupAlreadyExists": {
"placeholders": { "placeholders": {
@@ -1888,5 +1889,36 @@
"tcpErrorTimedOut": "Връзката TCP изтекла.", "tcpErrorTimedOut": "Връзката TCP изтекла.",
"tcpConnectionFailed": "Неуспешно е установено TCP връзката: {error}", "tcpConnectionFailed": "Неуспешно е установено TCP връзката: {error}",
"map_showDiscoveryContacts": "Покажи контакти за откриване", "map_showDiscoveryContacts": "Покажи контакти за откриване",
"map_setAsMyLocation": "Задайте като моя местоположение" "map_setAsMyLocation": "Задайте като моя местоположение",
"settings_denyAll": "Откажи всичко",
"settings_allowAll": "Позволи всичко",
"settings_allowByContact": "Позволи по флагове за контакт",
"settings_privacy": "Настройки на поверителността",
"settings_privacySettingsDescription": "Изберете каква информация устройството ви споделя с другите.",
"settings_privacySubtitle": "Контролирайте каква информация се споделя.",
"settings_telemetryBaseMode": "Базов режим на телеметрия",
"settings_telemetryLocationMode": "Режим на местоположение на телеметрията",
"settings_advertLocation": "Място на обявата",
"settings_advertLocationSubtitle": "Включи местоположение в обявата",
"contact_info": "Контактна информация",
"settings_telemetryEnvironmentMode": "Режим на средата на телеметрията",
"contact_telemetry": "Телеметрия",
"contact_lastSeen": "Последно видян",
"contact_clearChat": "Изчисти чата",
"contact_teleBase": "Базата данни за телеметрия",
"contact_settings": "Настройки за контакти",
"contact_teleBaseSubtitle": "Позволи споделяне на ниво на батерията и основна телеметрия",
"contact_teleEnv": "Среда на телеметрия",
"contact_teleLocSubtitle": "Позволи споделяне на данни за местоположение",
"contact_teleLoc": "Местоположение на телеметрията",
"contact_teleEnvSubtitle": "Позволи споделяне на данни от средносферните датчици",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"settings_multiAck": "Мулти-потвърди: {value}",
"settings_telemetryModeUpdated": "Режим на телеметрията е обновен"
} }
+33 -1
View File
@@ -285,6 +285,7 @@
"contacts_newGroup": "Neue Gruppe", "contacts_newGroup": "Neue Gruppe",
"contacts_groupName": "Gruppenname", "contacts_groupName": "Gruppenname",
"contacts_groupNameRequired": "Der Gruppennamen ist erforderlich.", "contacts_groupNameRequired": "Der Gruppennamen ist erforderlich.",
"contacts_groupNameReserved": "Dieser Gruppenname ist reserviert",
"contacts_groupAlreadyExists": "Die Gruppe \"{name}\" existiert bereits.", "contacts_groupAlreadyExists": "Die Gruppe \"{name}\" existiert bereits.",
"@contacts_groupAlreadyExists": { "@contacts_groupAlreadyExists": {
"placeholders": { "placeholders": {
@@ -1916,5 +1917,36 @@
"tcpErrorTimedOut": "Die TCP-Verbindung ist abgelaufen.", "tcpErrorTimedOut": "Die TCP-Verbindung ist abgelaufen.",
"tcpConnectionFailed": "Fehler beim TCP-Verbindungsaufbau: {error}", "tcpConnectionFailed": "Fehler beim TCP-Verbindungsaufbau: {error}",
"map_showDiscoveryContacts": "Entdeckungs-Kontakte anzeigen", "map_showDiscoveryContacts": "Entdeckungs-Kontakte anzeigen",
"map_setAsMyLocation": "Als meine aktuelle Position festlegen" "map_setAsMyLocation": "Als meine aktuelle Position festlegen",
"settings_allowByContact": "Zulassen durch Kontaktflaggen",
"settings_privacy": "Datenschutzeinstellungen",
"settings_allowAll": "Alles zulassen",
"settings_privacySettingsDescription": "Wählen Sie die Informationen, die Ihr Gerät mit anderen teilt.",
"settings_denyAll": "Alle ablehnen",
"settings_privacySubtitle": "Steuern Sie die Informationen, die freigegeben werden.",
"settings_telemetryLocationMode": "Telemetrie-Ortsmodus",
"settings_telemetryEnvironmentMode": "Telemetrie-Umgebungsmodus",
"settings_advertLocation": "Anzeigenort",
"settings_advertLocationSubtitle": "Ort in der Anzeige einbeziehen",
"settings_telemetryBaseMode": "Telemetrie-Basismodus",
"contact_teleBase": "Telemetriebasis",
"contact_teleBaseSubtitle": "Erlauben des Freigebens des Batteriestands und der grundlegenden Telemetrie",
"contact_teleLoc": "Telemetrieort",
"contact_teleLocSubtitle": "Teilen von Standortdaten zulassen",
"contact_info": "Kontaktinformationen",
"contact_settings": "Kontakteinstellungen",
"contact_telemetry": "Telemetrie",
"contact_teleEnv": "Telemetrieumgebung",
"contact_lastSeen": "Zuletzt gesehen",
"contact_clearChat": "Chat löschen",
"contact_teleEnvSubtitle": "Teilen von Umgebungsensordaten zulassen",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"settings_telemetryModeUpdated": "Telemetriemodus aktualisiert",
"settings_multiAck": "Mehrfach-Bestätigungen: {value}"
} }
+33 -1
View File
@@ -166,6 +166,26 @@
"settings_privacyModeToggle": "Toggle privacy mode to hide your name and location in advertisements.", "settings_privacyModeToggle": "Toggle privacy mode to hide your name and location in advertisements.",
"settings_privacyModeEnabled": "Privacy mode enabled", "settings_privacyModeEnabled": "Privacy mode enabled",
"settings_privacyModeDisabled": "Privacy mode disabled", "settings_privacyModeDisabled": "Privacy mode disabled",
"settings_privacy": "Privacy Settings",
"settings_privacySubtitle": "Control what information is shared.",
"settings_privacySettingsDescription": "Choose what information your device shares with others.",
"settings_denyAll": "Deny all",
"settings_allowByContact": "Allow by contact flags",
"settings_allowAll": "Allow all",
"settings_telemetryBaseMode": "Telemetry Base Mode",
"settings_telemetryLocationMode": "Telemetry Location Mode",
"settings_telemetryEnvironmentMode": "Telemetry Environment Mode",
"settings_advertLocation": "Advert Location",
"settings_advertLocationSubtitle": "Include location in advert.",
"settings_multiAck": "Multi-ACKs: {value}",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"settings_telemetryModeUpdated": "Telemetry mode updated",
"settings_actions": "Actions", "settings_actions": "Actions",
"settings_sendAdvertisement": "Send Advertisement", "settings_sendAdvertisement": "Send Advertisement",
"settings_sendAdvertisementSubtitle": "Broadcast presence now", "settings_sendAdvertisementSubtitle": "Broadcast presence now",
@@ -416,6 +436,7 @@
"contacts_newGroup": "New Group", "contacts_newGroup": "New Group",
"contacts_groupName": "Group name", "contacts_groupName": "Group name",
"contacts_groupNameRequired": "Group name is required", "contacts_groupNameRequired": "Group name is required",
"contacts_groupNameReserved": "This group name is reserved",
"contacts_groupAlreadyExists": "Group \"{name}\" already exists", "contacts_groupAlreadyExists": "Group \"{name}\" already exists",
"@contacts_groupAlreadyExists": { "@contacts_groupAlreadyExists": {
"placeholders": { "placeholders": {
@@ -454,6 +475,17 @@
} }
} }
}, },
"contact_info": "Contact Info",
"contact_settings": "Contact Settings",
"contact_telemetry": "Telemetry",
"contact_lastSeen": "Last seen",
"contact_clearChat": "Clear Chat",
"contact_teleBase": "Telemetry Base",
"contact_teleBaseSubtitle": "Allow sharing battery level and basic telemetry",
"contact_teleLoc": "Telemetry Location",
"contact_teleLocSubtitle": "Allow sharing location data",
"contact_teleEnv": "Telemetry Environment",
"contact_teleEnvSubtitle": "Allow sharing environment sensor data",
"channels_title": "Channels", "channels_title": "Channels",
"channels_noChannelsConfigured": "No channels configured", "channels_noChannelsConfigured": "No channels configured",
"channels_addPublicChannel": "Add Public Channel", "channels_addPublicChannel": "Add Public Channel",
@@ -1927,4 +1959,4 @@
"discoveredContacts_deleteContact": "Delete Discovered Contact", "discoveredContacts_deleteContact": "Delete Discovered Contact",
"discoveredContacts_deleteContactAll": "Delete All Discovered Contacts", "discoveredContacts_deleteContactAll": "Delete All Discovered Contacts",
"discoveredContacts_deleteContactAllContent": "Are you sure you want to delete all discovered contacts?" "discoveredContacts_deleteContactAllContent": "Are you sure you want to delete all discovered contacts?"
} }
+33 -1
View File
@@ -285,6 +285,7 @@
"contacts_newGroup": "Nuevo Grupo", "contacts_newGroup": "Nuevo Grupo",
"contacts_groupName": "Nombre del grupo", "contacts_groupName": "Nombre del grupo",
"contacts_groupNameRequired": "El nombre del grupo es obligatorio", "contacts_groupNameRequired": "El nombre del grupo es obligatorio",
"contacts_groupNameReserved": "Este nombre de grupo está reservado",
"contacts_groupAlreadyExists": "El grupo \"{name}\" ya existe", "contacts_groupAlreadyExists": "El grupo \"{name}\" ya existe",
"@contacts_groupAlreadyExists": { "@contacts_groupAlreadyExists": {
"placeholders": { "placeholders": {
@@ -1916,5 +1917,36 @@
"tcpErrorTimedOut": "La conexión TCP ha caducado.", "tcpErrorTimedOut": "La conexión TCP ha caducado.",
"tcpConnectionFailed": "Error en la conexión TCP: {error}", "tcpConnectionFailed": "Error en la conexión TCP: {error}",
"map_showDiscoveryContacts": "Mostrar Contactos de Descubrimiento", "map_showDiscoveryContacts": "Mostrar Contactos de Descubrimiento",
"map_setAsMyLocation": "Establecer mi ubicación" "map_setAsMyLocation": "Establecer mi ubicación",
"settings_privacySubtitle": "Controlar qué información se comparte.",
"settings_allowByContact": "Permitir por banderas de contacto",
"settings_denyAll": "Denegar todo",
"settings_telemetryBaseMode": "Modo base de telemetría",
"settings_telemetryEnvironmentMode": "Modo de entorno de telemetría",
"settings_advertLocationSubtitle": "Incluir ubicación en anuncio",
"contact_info": "Información de contacto",
"settings_privacySettingsDescription": "Elige qué información comparte tu dispositivo con otros.",
"settings_allowAll": "Permitir todo",
"settings_privacy": "Configuración de privacidad",
"contact_settings": "Configuración de contacto",
"settings_telemetryLocationMode": "Modo de ubicación de telemetría",
"contact_teleBase": "Base de Telemetría",
"contact_teleLoc": "Ubicación de telemetría",
"settings_advertLocation": "Ubicación de anuncio",
"contact_teleLocSubtitle": "Permitir el intercambio de datos de ubicación",
"contact_clearChat": "Borrar chat",
"contact_telemetry": "Telemetría",
"contact_lastSeen": "Visto por última vez",
"contact_teleBaseSubtitle": "Permitir el intercambio de nivel de batería y telemetría básica",
"contact_teleEnv": "Entorno de Telemetría",
"contact_teleEnvSubtitle": "Permitir el intercambio de datos de sensores de entorno",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"settings_telemetryModeUpdated": "Modo de telemetría actualizado",
"settings_multiAck": "Multi-ACKs: {value}"
} }
+33 -1
View File
@@ -285,6 +285,7 @@
"contacts_newGroup": "Nouveau Groupe", "contacts_newGroup": "Nouveau Groupe",
"contacts_groupName": "Nom du groupe", "contacts_groupName": "Nom du groupe",
"contacts_groupNameRequired": "Le nom du groupe est obligatoire.", "contacts_groupNameRequired": "Le nom du groupe est obligatoire.",
"contacts_groupNameReserved": "Ce nom de groupe est réservé",
"contacts_groupAlreadyExists": "Le groupe \"{name}\" existe déjà.", "contacts_groupAlreadyExists": "Le groupe \"{name}\" existe déjà.",
"@contacts_groupAlreadyExists": { "@contacts_groupAlreadyExists": {
"placeholders": { "placeholders": {
@@ -1888,5 +1889,36 @@
"tcpErrorTimedOut": "La connexion TCP a expiré.", "tcpErrorTimedOut": "La connexion TCP a expiré.",
"tcpConnectionFailed": "Échec de la connexion TCP : {error}", "tcpConnectionFailed": "Échec de la connexion TCP : {error}",
"map_showDiscoveryContacts": "Afficher les contacts de découverte", "map_showDiscoveryContacts": "Afficher les contacts de découverte",
"map_setAsMyLocation": "Définir comme ma localisation" "map_setAsMyLocation": "Définir comme ma localisation",
"settings_privacy": "Paramètres de confidentialité",
"settings_privacySubtitle": "Contrôlez les informations partagées",
"settings_telemetryLocationMode": "Mode d'emplacement de télémétrie",
"settings_telemetryEnvironmentMode": "Mode d'environnement de télémétrie",
"settings_advertLocation": "Emplacement de l'annonce",
"settings_advertLocationSubtitle": "Inclure l'emplacement dans l'annonce",
"settings_denyAll": "Refuser tout",
"settings_allowByContact": "Autoriser par drapeaux de contact",
"settings_privacySettingsDescription": "Choisissez les informations que votre appareil partage avec les autres.",
"settings_allowAll": "Autoriser tout",
"contact_info": "Informations de contact",
"settings_telemetryBaseMode": "Mode de base Télémétrie",
"contact_teleBase": "Base de télémétrie",
"contact_teleLoc": "Emplacement de télémétrie",
"contact_teleLocSubtitle": "Autoriser le partage des données de localisation",
"contact_teleEnv": "Environnement Télémétrie",
"contact_teleEnvSubtitle": "Autoriser le partage des données des capteurs d'environnement",
"contact_telemetry": "Télémétrie",
"contact_settings": "Paramètres de contact",
"contact_lastSeen": "Dernière fois vu",
"contact_clearChat": "Effacer la conversation",
"contact_teleBaseSubtitle": "Autoriser le partage du niveau de batterie et de la télémétrie de base",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"settings_multiAck": "Multi-ACKs : {value}",
"settings_telemetryModeUpdated": "Le mode télémétrie a été mis à jour"
} }
+33 -1
View File
@@ -285,6 +285,7 @@
"contacts_newGroup": "Nuovo Gruppo", "contacts_newGroup": "Nuovo Gruppo",
"contacts_groupName": "Nome gruppo", "contacts_groupName": "Nome gruppo",
"contacts_groupNameRequired": "Il nome del gruppo è obbligatorio.", "contacts_groupNameRequired": "Il nome del gruppo è obbligatorio.",
"contacts_groupNameReserved": "Questo nome del gruppo è riservato",
"contacts_groupAlreadyExists": "Il gruppo \"{name}\" esiste già.", "contacts_groupAlreadyExists": "Il gruppo \"{name}\" esiste già.",
"@contacts_groupAlreadyExists": { "@contacts_groupAlreadyExists": {
"placeholders": { "placeholders": {
@@ -1888,5 +1889,36 @@
"tcpErrorTimedOut": "La connessione TCP è scaduta.", "tcpErrorTimedOut": "La connessione TCP è scaduta.",
"tcpConnectionFailed": "Impossibile stabilire la connessione TCP: {error}", "tcpConnectionFailed": "Impossibile stabilire la connessione TCP: {error}",
"map_showDiscoveryContacts": "Mostra Contatti di Discovery", "map_showDiscoveryContacts": "Mostra Contatti di Discovery",
"map_setAsMyLocation": "Imposta come la mia posizione" "map_setAsMyLocation": "Imposta come la mia posizione",
"settings_privacySettingsDescription": "Scegli le informazioni che il tuo dispositivo condivide con gli altri.",
"settings_allowByContact": "Consenti in base ai flag di contatto",
"settings_telemetryLocationMode": "Modalità di posizionamento telemetrico",
"settings_telemetryEnvironmentMode": "Modalità di ambiente di telemetria",
"settings_advertLocation": "Posizione dell'annuncio",
"settings_advertLocationSubtitle": "Includi la posizione nell'annuncio",
"settings_privacy": "Impostazioni sulla privacy",
"settings_denyAll": "Negare tutto",
"settings_privacySubtitle": "Controlla le informazioni che vengono condivise.",
"settings_allowAll": "Consenti tutto",
"contact_info": "Informazioni di Contatto",
"settings_telemetryBaseMode": "Modalità di base di telemetria",
"contact_teleBase": "Base di telemetria",
"contact_teleLoc": "Posizione telemetria",
"contact_teleLocSubtitle": "Consenti la condivisione dei dati di posizione",
"contact_clearChat": "Cancella chat",
"contact_telemetry": "Telemetria",
"contact_settings": "Impostazioni di contatto",
"contact_lastSeen": "Ultimo accesso",
"contact_teleBaseSubtitle": "Consenti la condivisione del livello della batteria e della telemetria di base",
"contact_teleEnvSubtitle": "Consenti la condivisione dei dati del sensore ambientale",
"contact_teleEnv": "Ambiente di telemetria",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"settings_telemetryModeUpdated": "Modalità telemetria aggiornata",
"settings_multiAck": "Multi-ACKs: {value}"
} }
+150
View File
@@ -826,6 +826,84 @@ abstract class AppLocalizations {
/// **'Privacy mode disabled'** /// **'Privacy mode disabled'**
String get settings_privacyModeDisabled; String get settings_privacyModeDisabled;
/// No description provided for @settings_privacy.
///
/// In en, this message translates to:
/// **'Privacy Settings'**
String get settings_privacy;
/// No description provided for @settings_privacySubtitle.
///
/// In en, this message translates to:
/// **'Control what information is shared.'**
String get settings_privacySubtitle;
/// No description provided for @settings_privacySettingsDescription.
///
/// In en, this message translates to:
/// **'Choose what information your device shares with others.'**
String get settings_privacySettingsDescription;
/// No description provided for @settings_denyAll.
///
/// In en, this message translates to:
/// **'Deny all'**
String get settings_denyAll;
/// No description provided for @settings_allowByContact.
///
/// In en, this message translates to:
/// **'Allow by contact flags'**
String get settings_allowByContact;
/// No description provided for @settings_allowAll.
///
/// In en, this message translates to:
/// **'Allow all'**
String get settings_allowAll;
/// No description provided for @settings_telemetryBaseMode.
///
/// In en, this message translates to:
/// **'Telemetry Base Mode'**
String get settings_telemetryBaseMode;
/// No description provided for @settings_telemetryLocationMode.
///
/// In en, this message translates to:
/// **'Telemetry Location Mode'**
String get settings_telemetryLocationMode;
/// No description provided for @settings_telemetryEnvironmentMode.
///
/// In en, this message translates to:
/// **'Telemetry Environment Mode'**
String get settings_telemetryEnvironmentMode;
/// No description provided for @settings_advertLocation.
///
/// In en, this message translates to:
/// **'Advert Location'**
String get settings_advertLocation;
/// No description provided for @settings_advertLocationSubtitle.
///
/// In en, this message translates to:
/// **'Include location in advert.'**
String get settings_advertLocationSubtitle;
/// No description provided for @settings_multiAck.
///
/// In en, this message translates to:
/// **'Multi-ACKs: {value}'**
String settings_multiAck(String value);
/// No description provided for @settings_telemetryModeUpdated.
///
/// In en, this message translates to:
/// **'Telemetry mode updated'**
String get settings_telemetryModeUpdated;
/// No description provided for @settings_actions. /// No description provided for @settings_actions.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -1714,6 +1792,12 @@ abstract class AppLocalizations {
/// **'Group name is required'** /// **'Group name is required'**
String get contacts_groupNameRequired; String get contacts_groupNameRequired;
/// No description provided for @contacts_groupNameReserved.
///
/// In en, this message translates to:
/// **'This group name is reserved'**
String get contacts_groupNameReserved;
/// No description provided for @contacts_groupAlreadyExists. /// No description provided for @contacts_groupAlreadyExists.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -1774,6 +1858,72 @@ abstract class AppLocalizations {
/// **'~ {days} days'** /// **'~ {days} days'**
String contacts_lastSeenDaysAgo(int days); String contacts_lastSeenDaysAgo(int days);
/// No description provided for @contact_info.
///
/// In en, this message translates to:
/// **'Contact Info'**
String get contact_info;
/// No description provided for @contact_settings.
///
/// In en, this message translates to:
/// **'Contact Settings'**
String get contact_settings;
/// No description provided for @contact_telemetry.
///
/// In en, this message translates to:
/// **'Telemetry'**
String get contact_telemetry;
/// No description provided for @contact_lastSeen.
///
/// In en, this message translates to:
/// **'Last seen'**
String get contact_lastSeen;
/// No description provided for @contact_clearChat.
///
/// In en, this message translates to:
/// **'Clear Chat'**
String get contact_clearChat;
/// No description provided for @contact_teleBase.
///
/// In en, this message translates to:
/// **'Telemetry Base'**
String get contact_teleBase;
/// No description provided for @contact_teleBaseSubtitle.
///
/// In en, this message translates to:
/// **'Allow sharing battery level and basic telemetry'**
String get contact_teleBaseSubtitle;
/// No description provided for @contact_teleLoc.
///
/// In en, this message translates to:
/// **'Telemetry Location'**
String get contact_teleLoc;
/// No description provided for @contact_teleLocSubtitle.
///
/// In en, this message translates to:
/// **'Allow sharing location data'**
String get contact_teleLocSubtitle;
/// No description provided for @contact_teleEnv.
///
/// In en, this message translates to:
/// **'Telemetry Environment'**
String get contact_teleEnv;
/// No description provided for @contact_teleEnvSubtitle.
///
/// In en, this message translates to:
/// **'Allow sharing environment sensor data'**
String get contact_teleEnvSubtitle;
/// No description provided for @channels_title. /// No description provided for @channels_title.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
+85
View File
@@ -398,6 +398,52 @@ class AppLocalizationsBg extends AppLocalizations {
String get settings_privacyModeDisabled => String get settings_privacyModeDisabled =>
'Режим на поверителност е деактивиран'; 'Режим на поверителност е деактивиран';
@override
String get settings_privacy => 'Настройки на поверителността';
@override
String get settings_privacySubtitle =>
'Контролирайте каква информация се споделя.';
@override
String get settings_privacySettingsDescription =>
'Изберете каква информация устройството ви споделя с другите.';
@override
String get settings_denyAll => 'Откажи всичко';
@override
String get settings_allowByContact => 'Позволи по флагове за контакт';
@override
String get settings_allowAll => 'Позволи всичко';
@override
String get settings_telemetryBaseMode => 'Базов режим на телеметрия';
@override
String get settings_telemetryLocationMode =>
'Режим на местоположение на телеметрията';
@override
String get settings_telemetryEnvironmentMode =>
'Режим на средата на телеметрията';
@override
String get settings_advertLocation => 'Място на обявата';
@override
String get settings_advertLocationSubtitle =>
'Включи местоположение в обявата';
@override
String settings_multiAck(String value) {
return 'Мулти-потвърди: $value';
}
@override
String get settings_telemetryModeUpdated => 'Режим на телеметрията е обновен';
@override @override
String get settings_actions => 'Действия'; String get settings_actions => 'Действия';
@@ -902,6 +948,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get contacts_groupNameRequired => 'Името на групата е задължително.'; String get contacts_groupNameRequired => 'Името на групата е задължително.';
@override
String get contacts_groupNameReserved => 'Това име на група е запазено';
@override @override
String contacts_groupAlreadyExists(String name) { String contacts_groupAlreadyExists(String name) {
return 'Групата \"$name\" вече съществува.'; return 'Групата \"$name\" вече съществува.';
@@ -941,6 +990,42 @@ class AppLocalizationsBg extends AppLocalizations {
return 'Последно видян $days дни преди.'; return 'Последно видян $days дни преди.';
} }
@override
String get contact_info => 'Контактна информация';
@override
String get contact_settings => 'Настройки за контакти';
@override
String get contact_telemetry => 'Телеметрия';
@override
String get contact_lastSeen => 'Последно видян';
@override
String get contact_clearChat => 'Изчисти чата';
@override
String get contact_teleBase => 'Базата данни за телеметрия';
@override
String get contact_teleBaseSubtitle =>
'Позволи споделяне на ниво на батерията и основна телеметрия';
@override
String get contact_teleLoc => 'Местоположение на телеметрията';
@override
String get contact_teleLocSubtitle =>
'Позволи споделяне на данни за местоположение';
@override
String get contact_teleEnv => 'Среда на телеметрия';
@override
String get contact_teleEnvSubtitle =>
'Позволи споделяне на данни от средносферните датчици';
@override @override
String get channels_title => 'Канали'; String get channels_title => 'Канали';
+82
View File
@@ -398,6 +398,50 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get settings_privacyModeDisabled => 'Datenschutzmodus deaktiviert'; String get settings_privacyModeDisabled => 'Datenschutzmodus deaktiviert';
@override
String get settings_privacy => 'Datenschutzeinstellungen';
@override
String get settings_privacySubtitle =>
'Steuern Sie die Informationen, die freigegeben werden.';
@override
String get settings_privacySettingsDescription =>
'Wählen Sie die Informationen, die Ihr Gerät mit anderen teilt.';
@override
String get settings_denyAll => 'Alle ablehnen';
@override
String get settings_allowByContact => 'Zulassen durch Kontaktflaggen';
@override
String get settings_allowAll => 'Alles zulassen';
@override
String get settings_telemetryBaseMode => 'Telemetrie-Basismodus';
@override
String get settings_telemetryLocationMode => 'Telemetrie-Ortsmodus';
@override
String get settings_telemetryEnvironmentMode => 'Telemetrie-Umgebungsmodus';
@override
String get settings_advertLocation => 'Anzeigenort';
@override
String get settings_advertLocationSubtitle =>
'Ort in der Anzeige einbeziehen';
@override
String settings_multiAck(String value) {
return 'Mehrfach-Bestätigungen: $value';
}
@override
String get settings_telemetryModeUpdated => 'Telemetriemodus aktualisiert';
@override @override
String get settings_actions => 'Aktionen'; String get settings_actions => 'Aktionen';
@@ -902,6 +946,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get contacts_groupNameRequired => 'Der Gruppennamen ist erforderlich.'; String get contacts_groupNameRequired => 'Der Gruppennamen ist erforderlich.';
@override
String get contacts_groupNameReserved => 'Dieser Gruppenname ist reserviert';
@override @override
String contacts_groupAlreadyExists(String name) { String contacts_groupAlreadyExists(String name) {
return 'Die Gruppe \"$name\" existiert bereits.'; return 'Die Gruppe \"$name\" existiert bereits.';
@@ -941,6 +988,41 @@ class AppLocalizationsDe extends AppLocalizations {
return '~ $days Tage'; return '~ $days Tage';
} }
@override
String get contact_info => 'Kontaktinformationen';
@override
String get contact_settings => 'Kontakteinstellungen';
@override
String get contact_telemetry => 'Telemetrie';
@override
String get contact_lastSeen => 'Zuletzt gesehen';
@override
String get contact_clearChat => 'Chat löschen';
@override
String get contact_teleBase => 'Telemetriebasis';
@override
String get contact_teleBaseSubtitle =>
'Erlauben des Freigebens des Batteriestands und der grundlegenden Telemetrie';
@override
String get contact_teleLoc => 'Telemetrieort';
@override
String get contact_teleLocSubtitle => 'Teilen von Standortdaten zulassen';
@override
String get contact_teleEnv => 'Telemetrieumgebung';
@override
String get contact_teleEnvSubtitle =>
'Teilen von Umgebungsensordaten zulassen';
@override @override
String get channels_title => 'Kanäle'; String get channels_title => 'Kanäle';
+79
View File
@@ -392,6 +392,48 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get settings_privacyModeDisabled => 'Privacy mode disabled'; String get settings_privacyModeDisabled => 'Privacy mode disabled';
@override
String get settings_privacy => 'Privacy Settings';
@override
String get settings_privacySubtitle => 'Control what information is shared.';
@override
String get settings_privacySettingsDescription =>
'Choose what information your device shares with others.';
@override
String get settings_denyAll => 'Deny all';
@override
String get settings_allowByContact => 'Allow by contact flags';
@override
String get settings_allowAll => 'Allow all';
@override
String get settings_telemetryBaseMode => 'Telemetry Base Mode';
@override
String get settings_telemetryLocationMode => 'Telemetry Location Mode';
@override
String get settings_telemetryEnvironmentMode => 'Telemetry Environment Mode';
@override
String get settings_advertLocation => 'Advert Location';
@override
String get settings_advertLocationSubtitle => 'Include location in advert.';
@override
String settings_multiAck(String value) {
return 'Multi-ACKs: $value';
}
@override
String get settings_telemetryModeUpdated => 'Telemetry mode updated';
@override @override
String get settings_actions => 'Actions'; String get settings_actions => 'Actions';
@@ -889,6 +931,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get contacts_groupNameRequired => 'Group name is required'; String get contacts_groupNameRequired => 'Group name is required';
@override
String get contacts_groupNameReserved => 'This group name is reserved';
@override @override
String contacts_groupAlreadyExists(String name) { String contacts_groupAlreadyExists(String name) {
return 'Group \"$name\" already exists'; return 'Group \"$name\" already exists';
@@ -927,6 +972,40 @@ class AppLocalizationsEn extends AppLocalizations {
return '~ $days days'; return '~ $days days';
} }
@override
String get contact_info => 'Contact Info';
@override
String get contact_settings => 'Contact Settings';
@override
String get contact_telemetry => 'Telemetry';
@override
String get contact_lastSeen => 'Last seen';
@override
String get contact_clearChat => 'Clear Chat';
@override
String get contact_teleBase => 'Telemetry Base';
@override
String get contact_teleBaseSubtitle =>
'Allow sharing battery level and basic telemetry';
@override
String get contact_teleLoc => 'Telemetry Location';
@override
String get contact_teleLocSubtitle => 'Allow sharing location data';
@override
String get contact_teleEnv => 'Telemetry Environment';
@override
String get contact_teleEnvSubtitle => 'Allow sharing environment sensor data';
@override @override
String get channels_title => 'Channels'; String get channels_title => 'Channels';
+85
View File
@@ -396,6 +396,51 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get settings_privacyModeDisabled => 'Modo de privacidad desactivado'; String get settings_privacyModeDisabled => 'Modo de privacidad desactivado';
@override
String get settings_privacy => 'Configuración de privacidad';
@override
String get settings_privacySubtitle =>
'Controlar qué información se comparte.';
@override
String get settings_privacySettingsDescription =>
'Elige qué información comparte tu dispositivo con otros.';
@override
String get settings_denyAll => 'Denegar todo';
@override
String get settings_allowByContact => 'Permitir por banderas de contacto';
@override
String get settings_allowAll => 'Permitir todo';
@override
String get settings_telemetryBaseMode => 'Modo base de telemetría';
@override
String get settings_telemetryLocationMode =>
'Modo de ubicación de telemetría';
@override
String get settings_telemetryEnvironmentMode =>
'Modo de entorno de telemetría';
@override
String get settings_advertLocation => 'Ubicación de anuncio';
@override
String get settings_advertLocationSubtitle => 'Incluir ubicación en anuncio';
@override
String settings_multiAck(String value) {
return 'Multi-ACKs: $value';
}
@override
String get settings_telemetryModeUpdated => 'Modo de telemetría actualizado';
@override @override
String get settings_actions => 'Acciones'; String get settings_actions => 'Acciones';
@@ -901,6 +946,10 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get contacts_groupNameRequired => 'El nombre del grupo es obligatorio'; String get contacts_groupNameRequired => 'El nombre del grupo es obligatorio';
@override
String get contacts_groupNameReserved =>
'Este nombre de grupo está reservado';
@override @override
String contacts_groupAlreadyExists(String name) { String contacts_groupAlreadyExists(String name) {
return 'El grupo \"$name\" ya existe'; return 'El grupo \"$name\" ya existe';
@@ -940,6 +989,42 @@ class AppLocalizationsEs extends AppLocalizations {
return '~ $days días'; return '~ $days días';
} }
@override
String get contact_info => 'Información de contacto';
@override
String get contact_settings => 'Configuración de contacto';
@override
String get contact_telemetry => 'Telemetría';
@override
String get contact_lastSeen => 'Visto por última vez';
@override
String get contact_clearChat => 'Borrar chat';
@override
String get contact_teleBase => 'Base de Telemetría';
@override
String get contact_teleBaseSubtitle =>
'Permitir el intercambio de nivel de batería y telemetría básica';
@override
String get contact_teleLoc => 'Ubicación de telemetría';
@override
String get contact_teleLocSubtitle =>
'Permitir el intercambio de datos de ubicación';
@override
String get contact_teleEnv => 'Entorno de Telemetría';
@override
String get contact_teleEnvSubtitle =>
'Permitir el intercambio de datos de sensores de entorno';
@override @override
String get channels_title => 'Canales'; String get channels_title => 'Canales';
+85
View File
@@ -400,6 +400,52 @@ class AppLocalizationsFr extends AppLocalizations {
String get settings_privacyModeDisabled => String get settings_privacyModeDisabled =>
'Mode de confidentialité désactivé'; 'Mode de confidentialité désactivé';
@override
String get settings_privacy => 'Paramètres de confidentialité';
@override
String get settings_privacySubtitle => 'Contrôlez les informations partagées';
@override
String get settings_privacySettingsDescription =>
'Choisissez les informations que votre appareil partage avec les autres.';
@override
String get settings_denyAll => 'Refuser tout';
@override
String get settings_allowByContact => 'Autoriser par drapeaux de contact';
@override
String get settings_allowAll => 'Autoriser tout';
@override
String get settings_telemetryBaseMode => 'Mode de base Télémétrie';
@override
String get settings_telemetryLocationMode =>
'Mode d\'emplacement de télémétrie';
@override
String get settings_telemetryEnvironmentMode =>
'Mode d\'environnement de télémétrie';
@override
String get settings_advertLocation => 'Emplacement de l\'annonce';
@override
String get settings_advertLocationSubtitle =>
'Inclure l\'emplacement dans l\'annonce';
@override
String settings_multiAck(String value) {
return 'Multi-ACKs : $value';
}
@override
String get settings_telemetryModeUpdated =>
'Le mode télémétrie a été mis à jour';
@override @override
String get settings_actions => 'Actions'; String get settings_actions => 'Actions';
@@ -905,6 +951,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get contacts_groupNameRequired => 'Le nom du groupe est obligatoire.'; String get contacts_groupNameRequired => 'Le nom du groupe est obligatoire.';
@override
String get contacts_groupNameReserved => 'Ce nom de groupe est réservé';
@override @override
String contacts_groupAlreadyExists(String name) { String contacts_groupAlreadyExists(String name) {
return 'Le groupe \"$name\" existe déjà.'; return 'Le groupe \"$name\" existe déjà.';
@@ -944,6 +993,42 @@ class AppLocalizationsFr extends AppLocalizations {
return '~ $days jours'; return '~ $days jours';
} }
@override
String get contact_info => 'Informations de contact';
@override
String get contact_settings => 'Paramètres de contact';
@override
String get contact_telemetry => 'Télémétrie';
@override
String get contact_lastSeen => 'Dernière fois vu';
@override
String get contact_clearChat => 'Effacer la conversation';
@override
String get contact_teleBase => 'Base de télémétrie';
@override
String get contact_teleBaseSubtitle =>
'Autoriser le partage du niveau de batterie et de la télémétrie de base';
@override
String get contact_teleLoc => 'Emplacement de télémétrie';
@override
String get contact_teleLocSubtitle =>
'Autoriser le partage des données de localisation';
@override
String get contact_teleEnv => 'Environnement Télémétrie';
@override
String get contact_teleEnvSubtitle =>
'Autoriser le partage des données des capteurs d\'environnement';
@override @override
String get channels_title => 'Canaux'; String get channels_title => 'Canaux';
+85
View File
@@ -398,6 +398,52 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get settings_privacyModeDisabled => 'Modalità privacy disabilitata'; String get settings_privacyModeDisabled => 'Modalità privacy disabilitata';
@override
String get settings_privacy => 'Impostazioni sulla privacy';
@override
String get settings_privacySubtitle =>
'Controlla le informazioni che vengono condivise.';
@override
String get settings_privacySettingsDescription =>
'Scegli le informazioni che il tuo dispositivo condivide con gli altri.';
@override
String get settings_denyAll => 'Negare tutto';
@override
String get settings_allowByContact => 'Consenti in base ai flag di contatto';
@override
String get settings_allowAll => 'Consenti tutto';
@override
String get settings_telemetryBaseMode => 'Modalità di base di telemetria';
@override
String get settings_telemetryLocationMode =>
'Modalità di posizionamento telemetrico';
@override
String get settings_telemetryEnvironmentMode =>
'Modalità di ambiente di telemetria';
@override
String get settings_advertLocation => 'Posizione dell\'annuncio';
@override
String get settings_advertLocationSubtitle =>
'Includi la posizione nell\'annuncio';
@override
String settings_multiAck(String value) {
return 'Multi-ACKs: $value';
}
@override
String get settings_telemetryModeUpdated => 'Modalità telemetria aggiornata';
@override @override
String get settings_actions => 'Azioni'; String get settings_actions => 'Azioni';
@@ -901,6 +947,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get contacts_groupNameRequired => 'Il nome del gruppo è obbligatorio.'; String get contacts_groupNameRequired => 'Il nome del gruppo è obbligatorio.';
@override
String get contacts_groupNameReserved => 'Questo nome del gruppo è riservato';
@override @override
String contacts_groupAlreadyExists(String name) { String contacts_groupAlreadyExists(String name) {
return 'Il gruppo \"$name\" esiste già.'; return 'Il gruppo \"$name\" esiste già.';
@@ -940,6 +989,42 @@ class AppLocalizationsIt extends AppLocalizations {
return 'Ultimo visto $days giorni fa'; return 'Ultimo visto $days giorni fa';
} }
@override
String get contact_info => 'Informazioni di Contatto';
@override
String get contact_settings => 'Impostazioni di contatto';
@override
String get contact_telemetry => 'Telemetria';
@override
String get contact_lastSeen => 'Ultimo accesso';
@override
String get contact_clearChat => 'Cancella chat';
@override
String get contact_teleBase => 'Base di telemetria';
@override
String get contact_teleBaseSubtitle =>
'Consenti la condivisione del livello della batteria e della telemetria di base';
@override
String get contact_teleLoc => 'Posizione telemetria';
@override
String get contact_teleLocSubtitle =>
'Consenti la condivisione dei dati di posizione';
@override
String get contact_teleEnv => 'Ambiente di telemetria';
@override
String get contact_teleEnvSubtitle =>
'Consenti la condivisione dei dati del sensore ambientale';
@override @override
String get channels_title => 'Canali'; String get channels_title => 'Canali';
+81
View File
@@ -395,6 +395,50 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get settings_privacyModeDisabled => 'Privacy modus is uitgeschakeld'; String get settings_privacyModeDisabled => 'Privacy modus is uitgeschakeld';
@override
String get settings_privacy => 'Privacyinstellingen';
@override
String get settings_privacySubtitle =>
'Beheer welke informatie wordt gedeeld';
@override
String get settings_privacySettingsDescription =>
'Kies welke informatie uw apparaat deelt met anderen';
@override
String get settings_denyAll => 'Weiger alles';
@override
String get settings_allowByContact => 'Toestaan op basis van contactvlaggen';
@override
String get settings_allowAll => 'Alles toestaan';
@override
String get settings_telemetryBaseMode => 'Telemetrie-basismodus';
@override
String get settings_telemetryLocationMode => 'Telemetrie-locatiemodus';
@override
String get settings_telemetryEnvironmentMode => 'Telemetrie-omgevingsmodus';
@override
String get settings_advertLocation => 'Advertentielocatie';
@override
String get settings_advertLocationSubtitle =>
'Locatie opnemen in advertentie';
@override
String settings_multiAck(String value) {
return 'Multi-ACKs: $value';
}
@override
String get settings_telemetryModeUpdated => 'Telemetrie-modus bijgewerkt';
@override @override
String get settings_actions => 'Acties'; String get settings_actions => 'Acties';
@@ -895,6 +939,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get contacts_groupNameRequired => 'De groepnaam is verplicht.'; String get contacts_groupNameRequired => 'De groepnaam is verplicht.';
@override
String get contacts_groupNameReserved => 'Deze groepsnaam is gereserveerd';
@override @override
String contacts_groupAlreadyExists(String name) { String contacts_groupAlreadyExists(String name) {
return 'De groep \"$name\" bestaat al.'; return 'De groep \"$name\" bestaat al.';
@@ -934,6 +981,40 @@ class AppLocalizationsNl extends AppLocalizations {
return 'Laast gezien $days dagen geleden'; return 'Laast gezien $days dagen geleden';
} }
@override
String get contact_info => 'Contactinformatie';
@override
String get contact_settings => 'Contactinstellingen';
@override
String get contact_telemetry => 'Telemetrie';
@override
String get contact_lastSeen => 'Laatst gezien';
@override
String get contact_clearChat => 'Chat leegmaken';
@override
String get contact_teleBase => 'Telemetrie_basis';
@override
String get contact_teleBaseSubtitle =>
'Sta delen van batterij niveau en basis telemetrie toe';
@override
String get contact_teleLoc => 'Telemetrielocatie';
@override
String get contact_teleLocSubtitle => 'Locatiegegevens delen toestaan';
@override
String get contact_teleEnv => 'Telemetrieomgeving';
@override
String get contact_teleEnvSubtitle => 'Delen van omgevingsensordata toestaan';
@override @override
String get channels_title => 'Kanaal'; String get channels_title => 'Kanaal';
+85
View File
@@ -401,6 +401,52 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get settings_privacyModeDisabled => 'Tryb prywatności wyłączony'; String get settings_privacyModeDisabled => 'Tryb prywatności wyłączony';
@override
String get settings_privacy => 'Ustawienia prywatności';
@override
String get settings_privacySubtitle =>
'Kontroluj jakie informacje są udostępniane.';
@override
String get settings_privacySettingsDescription =>
'Wybierz jakie informacje urządzenie udostępni innym.';
@override
String get settings_denyAll => 'Odmów wszystkim';
@override
String get settings_allowByContact => 'Zezwalaj według flag kontaktowych';
@override
String get settings_allowAll => 'Zezwalaj na wszystko';
@override
String get settings_telemetryBaseMode => 'Tryb podstawowy telemetrii';
@override
String get settings_telemetryLocationMode => 'Tryb położenia telemetrycznego';
@override
String get settings_telemetryEnvironmentMode =>
'Tryb środowiska telemetrycznego';
@override
String get settings_advertLocation => 'Lokalizacja reklamowa';
@override
String get settings_advertLocationSubtitle =>
'Uwzględnij lokalizację w ogłoszeniu';
@override
String settings_multiAck(String value) {
return 'Wiele potwierdzeń: $value';
}
@override
String get settings_telemetryModeUpdated =>
'Tryb telemetryczny zaktualizowany';
@override @override
String get settings_actions => 'Działania'; String get settings_actions => 'Działania';
@@ -904,6 +950,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get contacts_groupNameRequired => 'Nazwa grupy jest wymagana'; String get contacts_groupNameRequired => 'Nazwa grupy jest wymagana';
@override
String get contacts_groupNameReserved => 'Ta nazwa grupy jest zastrzeżona';
@override @override
String contacts_groupAlreadyExists(String name) { String contacts_groupAlreadyExists(String name) {
return 'Grupa \"$name\" już istnieje'; return 'Grupa \"$name\" już istnieje';
@@ -943,6 +992,42 @@ class AppLocalizationsPl extends AppLocalizations {
return 'Ostatnie połączenie $days dni temu'; return 'Ostatnie połączenie $days dni temu';
} }
@override
String get contact_info => 'Informacje kontaktowe';
@override
String get contact_settings => 'Ustawienia kontaktowe';
@override
String get contact_telemetry => 'Telemetryka';
@override
String get contact_lastSeen => 'Ostatnio widziany';
@override
String get contact_clearChat => 'Wyczyść czat';
@override
String get contact_teleBase => 'Baza telemetryczna';
@override
String get contact_teleBaseSubtitle =>
'Pozwól na udostępnianie poziomu naładowania baterii i podstawowych danych telemetrycznych';
@override
String get contact_teleLoc => 'Lokalizacja telemetryczna';
@override
String get contact_teleLocSubtitle =>
'Zezwalaj na udostępnianie danych lokalizacji';
@override
String get contact_teleEnv => 'Środowisko telemetryczne';
@override
String get contact_teleEnvSubtitle =>
'Zezwalaj na udostępnianie danych czujników środowiskowych';
@override @override
String get channels_title => 'Kanały'; String get channels_title => 'Kanały';
+84
View File
@@ -398,6 +398,51 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get settings_privacyModeDisabled => 'Modo de privacidade desativado'; String get settings_privacyModeDisabled => 'Modo de privacidade desativado';
@override
String get settings_privacy => 'Configurações de Privacidade';
@override
String get settings_privacySubtitle => 'Controle o que é compartilhado.';
@override
String get settings_privacySettingsDescription =>
'Escolha quais informações o seu dispositivo compartilha com os outros.';
@override
String get settings_denyAll => 'Negar todos';
@override
String get settings_allowByContact => 'Permitir por bandeiras de contato';
@override
String get settings_allowAll => 'Permitir todos';
@override
String get settings_telemetryBaseMode => 'Modo Base de Telemetria';
@override
String get settings_telemetryLocationMode =>
'Modo de Localização de Telemetria';
@override
String get settings_telemetryEnvironmentMode =>
'Modo de Ambiente de Telemetria';
@override
String get settings_advertLocation => 'Localização do Anúncio';
@override
String get settings_advertLocationSubtitle =>
'Incluir localização no anúncio';
@override
String settings_multiAck(String value) {
return 'Multi-ACKs: $value';
}
@override
String get settings_telemetryModeUpdated => 'Modo de telemetria atualizado';
@override @override
String get settings_actions => 'Ações'; String get settings_actions => 'Ações';
@@ -903,6 +948,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get contacts_groupNameRequired => 'O nome do grupo é obrigatório.'; String get contacts_groupNameRequired => 'O nome do grupo é obrigatório.';
@override
String get contacts_groupNameReserved => 'Este nome de grupo está reservado';
@override @override
String contacts_groupAlreadyExists(String name) { String contacts_groupAlreadyExists(String name) {
return 'O grupo \"$name\" já existe'; return 'O grupo \"$name\" já existe';
@@ -942,6 +990,42 @@ class AppLocalizationsPt extends AppLocalizations {
return 'Última vez visto $days dias atrás'; return 'Última vez visto $days dias atrás';
} }
@override
String get contact_info => 'Informações de Contato';
@override
String get contact_settings => 'Configurações de Contato';
@override
String get contact_telemetry => 'Telemetria';
@override
String get contact_lastSeen => 'Visto pela última vez';
@override
String get contact_clearChat => 'Limpar Chat';
@override
String get contact_teleBase => 'Base de Telemetria';
@override
String get contact_teleBaseSubtitle =>
'Permitir compartilhamento do nível da bateria e telemetria básica';
@override
String get contact_teleLoc => 'Localização de Telemetria';
@override
String get contact_teleLocSubtitle =>
'Permitir compartilhamento de dados de localização';
@override
String get contact_teleEnv => 'Ambiente de Telemetria';
@override
String get contact_teleEnvSubtitle =>
'Permitir compartilhamento de dados do sensor de ambiente';
@override @override
String get channels_title => 'Canais'; String get channels_title => 'Canais';
+84
View File
@@ -398,6 +398,51 @@ class AppLocalizationsRu extends AppLocalizations {
String get settings_privacyModeDisabled => String get settings_privacyModeDisabled =>
'Режим конфиденциальности выключен'; 'Режим конфиденциальности выключен';
@override
String get settings_privacy => 'Настройки конфиденциальности';
@override
String get settings_privacySubtitle =>
'Контролируйте, какую информацию делиться.';
@override
String get settings_privacySettingsDescription =>
'Выберите, какую информацию ваше устройство будет делиться с другими.';
@override
String get settings_denyAll => 'Отклонить все';
@override
String get settings_allowByContact => 'Разрешить по флагам контактов';
@override
String get settings_allowAll => 'Разрешить все';
@override
String get settings_telemetryBaseMode => 'Базовый режим телеметрии';
@override
String get settings_telemetryLocationMode =>
'Режим местоположения телеметрии';
@override
String get settings_telemetryEnvironmentMode => 'Режим среды телеметрии';
@override
String get settings_advertLocation => 'Местоположение рекламы';
@override
String get settings_advertLocationSubtitle =>
'Включить местоположение в объявление';
@override
String settings_multiAck(String value) {
return 'Мульти-ACK: $value';
}
@override
String get settings_telemetryModeUpdated => 'Режим телеметрии обновлен';
@override @override
String get settings_actions => 'Действия'; String get settings_actions => 'Действия';
@@ -902,6 +947,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get contacts_groupNameRequired => 'Имя группы обязательно'; String get contacts_groupNameRequired => 'Имя группы обязательно';
@override
String get contacts_groupNameReserved => 'Это имя группы зарезервировано';
@override @override
String contacts_groupAlreadyExists(String name) { String contacts_groupAlreadyExists(String name) {
return 'Группа \"$name\" уже существует'; return 'Группа \"$name\" уже существует';
@@ -941,6 +989,42 @@ class AppLocalizationsRu extends AppLocalizations {
return 'Видели $days дн. назад'; return 'Видели $days дн. назад';
} }
@override
String get contact_info => 'Контактная информация';
@override
String get contact_settings => 'Настройки контактов';
@override
String get contact_telemetry => 'Телеметрия';
@override
String get contact_lastSeen => 'Последний раз видели';
@override
String get contact_clearChat => 'Очистить чат';
@override
String get contact_teleBase => 'База телеметрии';
@override
String get contact_teleBaseSubtitle =>
'Разрешить обмен уровнем заряда батареи и базовой телеметрией';
@override
String get contact_teleLoc => 'Местоположение телеметрии';
@override
String get contact_teleLocSubtitle =>
'Разрешить обмен данными о местоположении';
@override
String get contact_teleEnv => 'Среда телеметрии';
@override
String get contact_teleEnvSubtitle =>
'Разрешить обмен данными датчиков окружающей среды';
@override @override
String get channels_title => 'Каналы'; String get channels_title => 'Каналы';
+81
View File
@@ -395,6 +395,49 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get settings_privacyModeDisabled => 'Ochranný režim je vypnutý'; String get settings_privacyModeDisabled => 'Ochranný režim je vypnutý';
@override
String get settings_privacy => 'Nastavenia súkromia';
@override
String get settings_privacySubtitle => 'Ovládni, aké informácie sa zdieľajú.';
@override
String get settings_privacySettingsDescription =>
'Vyberte, ktoré informácie váš zariadenie zdieľa s ostatnými.';
@override
String get settings_denyAll => 'Zamietnuť všetko';
@override
String get settings_allowByContact => 'Povoliť podľa kontaktových vlajok';
@override
String get settings_allowAll => 'Povoliť všetko';
@override
String get settings_telemetryBaseMode => 'Základný režim telemetrie';
@override
String get settings_telemetryLocationMode => 'Režim umiestnenia telemetrie';
@override
String get settings_telemetryEnvironmentMode => 'Režim prostredia telemetrie';
@override
String get settings_advertLocation => 'Umiestnenie inzerátu';
@override
String get settings_advertLocationSubtitle => 'Zahrnúť polohu do inzerátu';
@override
String settings_multiAck(String value) {
return 'Viaceré ACK: $value';
}
@override
String get settings_telemetryModeUpdated =>
'Režim telemetrie bol aktualizovaný';
@override @override
String get settings_actions => 'Možné akcie'; String get settings_actions => 'Možné akcie';
@@ -894,6 +937,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get contacts_groupNameRequired => 'Skupina musí mať názov.'; String get contacts_groupNameRequired => 'Skupina musí mať názov.';
@override
String get contacts_groupNameReserved => 'Tento názov skupiny je rezervovaný';
@override @override
String contacts_groupAlreadyExists(String name) { String contacts_groupAlreadyExists(String name) {
return 'Skupina \"$name\" už existuje'; return 'Skupina \"$name\" už existuje';
@@ -935,6 +981,41 @@ class AppLocalizationsSk extends AppLocalizations {
return 'Posledné zobrazenie $days dní dozadu'; return 'Posledné zobrazenie $days dní dozadu';
} }
@override
String get contact_info => 'Kontaktné informácie';
@override
String get contact_settings => 'Nastavenia kontaktov';
@override
String get contact_telemetry => 'Telemetria';
@override
String get contact_lastSeen => 'Naposledy videný';
@override
String get contact_clearChat => 'Vymazať chat';
@override
String get contact_teleBase => 'Báza telemetrie';
@override
String get contact_teleBaseSubtitle =>
'Povoliť zdieľanie úrovne batérie a základnej telemetrie';
@override
String get contact_teleLoc => 'Lokácia telemetrie';
@override
String get contact_teleLocSubtitle => 'Povoliť zdieľanie údajov o lokalite';
@override
String get contact_teleEnv => 'Prostredie telemetrie';
@override
String get contact_teleEnvSubtitle =>
'Povoliť zdieľanie údajov senzorov prostredia';
@override @override
String get channels_title => 'Kanály'; String get channels_title => 'Kanály';
+82
View File
@@ -393,6 +393,50 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get settings_privacyModeDisabled => 'Privatni način je onemogočen.'; String get settings_privacyModeDisabled => 'Privatni način je onemogočen.';
@override
String get settings_privacy => 'Nastavitve zasebnosti';
@override
String get settings_privacySubtitle =>
'Kontrolirajte, katere informacije so deljene.';
@override
String get settings_privacySettingsDescription =>
'Izberite, katere informacije vaš naprava deli z drugimi.';
@override
String get settings_denyAll => 'Zavrniti vse';
@override
String get settings_allowByContact => 'Dovoli po kontaktnih zastavah';
@override
String get settings_allowAll => 'Dovoli vse';
@override
String get settings_telemetryBaseMode => 'Osnovni način telemetrije';
@override
String get settings_telemetryLocationMode => 'Način delovanja telemetrije';
@override
String get settings_telemetryEnvironmentMode =>
'Način delovanja okolja telemetrije';
@override
String get settings_advertLocation => 'Lokacija oglasa';
@override
String get settings_advertLocationSubtitle => 'Vključi lokacijo v oglas.';
@override
String settings_multiAck(String value) {
return 'Večkratni potrditvi: $value';
}
@override
String get settings_telemetryModeUpdated => 'Način telemetrije posodobljen';
@override @override
String get settings_actions => 'Akcije'; String get settings_actions => 'Akcije';
@@ -892,6 +936,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get contacts_groupNameRequired => 'Ime skupine je obvezno.'; String get contacts_groupNameRequired => 'Ime skupine je obvezno.';
@override
String get contacts_groupNameReserved => 'To ime skupine je rezervirano';
@override @override
String contacts_groupAlreadyExists(String name) { String contacts_groupAlreadyExists(String name) {
return 'Skupina \"$name\" že obstaja'; return 'Skupina \"$name\" že obstaja';
@@ -931,6 +978,41 @@ class AppLocalizationsSl extends AppLocalizations {
return 'Zadnjič viden pred $days dnem'; return 'Zadnjič viden pred $days dnem';
} }
@override
String get contact_info => 'Kontaktni podatki';
@override
String get contact_settings => 'Nastavitve stika';
@override
String get contact_telemetry => 'Telemetrija';
@override
String get contact_lastSeen => 'Zadnjič videno';
@override
String get contact_clearChat => 'Počisti klepet';
@override
String get contact_teleBase => 'Baza telemetrije';
@override
String get contact_teleBaseSubtitle =>
'Dovoli deljenje stanja baterije in osnovne telemetrije';
@override
String get contact_teleLoc => 'Lokacija telemetrije';
@override
String get contact_teleLocSubtitle => 'Dovoli deljenje podatkov o lokaciji';
@override
String get contact_teleEnv => 'Okolje telemetrije';
@override
String get contact_teleEnvSubtitle =>
'Dovoli deljenje podatkov okoljskih senzorjev';
@override @override
String get channels_title => 'Kanali'; String get channels_title => 'Kanali';
+80
View File
@@ -392,6 +392,49 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get settings_privacyModeDisabled => 'Privatläge är avstängt'; String get settings_privacyModeDisabled => 'Privatläge är avstängt';
@override
String get settings_privacy => 'Inställningar för sekretess';
@override
String get settings_privacySubtitle =>
'Kontrollera vilken information som delas.';
@override
String get settings_privacySettingsDescription =>
'Välj vilken information din enhet delar med andra.';
@override
String get settings_denyAll => 'Neka alla';
@override
String get settings_allowByContact => 'Tillåt via kontaktflaggor';
@override
String get settings_allowAll => 'Tillåt alla';
@override
String get settings_telemetryBaseMode => 'Telemetribasläge';
@override
String get settings_telemetryLocationMode => 'Telemetritillstånd för plats';
@override
String get settings_telemetryEnvironmentMode => 'Telemetri miljöläge';
@override
String get settings_advertLocation => 'Annonsplacering';
@override
String get settings_advertLocationSubtitle => 'Inkludera plats i annonsen';
@override
String settings_multiAck(String value) {
return 'Multi-ACKs: $value';
}
@override
String get settings_telemetryModeUpdated => 'Telemetri-läge uppdaterat';
@override @override
String get settings_actions => 'Åtgärder'; String get settings_actions => 'Åtgärder';
@@ -888,6 +931,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get contacts_groupNameRequired => 'Gruppnamnet är obligatoriskt'; String get contacts_groupNameRequired => 'Gruppnamnet är obligatoriskt';
@override
String get contacts_groupNameReserved => 'Detta gruppnamn är reserverat';
@override @override
String contacts_groupAlreadyExists(String name) { String contacts_groupAlreadyExists(String name) {
return 'Gruppen \"$name\" finns redan.'; return 'Gruppen \"$name\" finns redan.';
@@ -927,6 +973,40 @@ class AppLocalizationsSv extends AppLocalizations {
return 'Senast synlig $days dagar sedan'; return 'Senast synlig $days dagar sedan';
} }
@override
String get contact_info => 'Kontaktinformation';
@override
String get contact_settings => 'Kontaktinställningar';
@override
String get contact_telemetry => 'Telemetri';
@override
String get contact_lastSeen => 'Senast sedd';
@override
String get contact_clearChat => 'Rensa Chatt';
@override
String get contact_teleBase => 'Telemetribas';
@override
String get contact_teleBaseSubtitle =>
'Tillåt delning av batterinivå och grundläggande telemetri';
@override
String get contact_teleLoc => 'Telemetridata plats';
@override
String get contact_teleLocSubtitle => 'Tillåt delning av platsdata';
@override
String get contact_teleEnv => 'Telemetri Miljö';
@override
String get contact_teleEnvSubtitle => 'Tillåt delning av miljösensordata';
@override @override
String get channels_title => 'Kanaler'; String get channels_title => 'Kanaler';
+83
View File
@@ -395,6 +395,50 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get settings_privacyModeDisabled => 'Режим приватності вимкнено'; String get settings_privacyModeDisabled => 'Режим приватності вимкнено';
@override
String get settings_privacy => 'Налаштування приватності';
@override
String get settings_privacySubtitle =>
'Керуйте інформацією, яку буде спільно використовуватися';
@override
String get settings_privacySettingsDescription =>
'Виберіть, яку інформацію ваш пристрій буде передавати іншим.';
@override
String get settings_denyAll => 'Відхилити все';
@override
String get settings_allowByContact => 'Дозволити за контактними прапорцями';
@override
String get settings_allowAll => 'Дозволити все';
@override
String get settings_telemetryBaseMode => 'Режим базової телеметрії';
@override
String get settings_telemetryLocationMode => 'Режим місця телеметрії';
@override
String get settings_telemetryEnvironmentMode => 'Режим середовища телеметрії';
@override
String get settings_advertLocation => 'Розміщення реклами';
@override
String get settings_advertLocationSubtitle =>
'Включити місце розташування в оголошення';
@override
String settings_multiAck(String value) {
return 'Багатократне підтвердження: $value';
}
@override
String get settings_telemetryModeUpdated => 'Режим телеметрії оновлено';
@override @override
String get settings_actions => 'Дії'; String get settings_actions => 'Дії';
@@ -898,6 +942,9 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get contacts_groupNameRequired => 'Назва групи обов\'язкова.'; String get contacts_groupNameRequired => 'Назва групи обов\'язкова.';
@override
String get contacts_groupNameReserved => 'Ця назва групи зарезервована';
@override @override
String contacts_groupAlreadyExists(String name) { String contacts_groupAlreadyExists(String name) {
return 'Група «$name» вже існує.'; return 'Група «$name» вже існує.';
@@ -937,6 +984,42 @@ class AppLocalizationsUk extends AppLocalizations {
return 'В мережі $days дн. тому'; return 'В мережі $days дн. тому';
} }
@override
String get contact_info => 'Контактна інформація';
@override
String get contact_settings => 'Налаштування контактів';
@override
String get contact_telemetry => 'Телеметрія';
@override
String get contact_lastSeen => 'Останній раз бачили';
@override
String get contact_clearChat => 'Очистити чат';
@override
String get contact_teleBase => 'Базовий телебачення';
@override
String get contact_teleBaseSubtitle =>
'Дозволити спільний доступ до рівня заряду батареї та базової телеметрії';
@override
String get contact_teleLoc => 'Розташування телеметрії';
@override
String get contact_teleLocSubtitle =>
'Дозволити спільне використання даних про місцеположення';
@override
String get contact_teleEnv => 'Середовище телеметрії';
@override
String get contact_teleEnvSubtitle =>
'Дозволити спільний доступ до даних датчиків середовища';
@override @override
String get channels_title => 'Канали'; String get channels_title => 'Канали';
+77
View File
@@ -374,6 +374,47 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get settings_privacyModeDisabled => '隐私模式已关闭'; String get settings_privacyModeDisabled => '隐私模式已关闭';
@override
String get settings_privacy => '隐私设置';
@override
String get settings_privacySubtitle => '控制要共享的信息。';
@override
String get settings_privacySettingsDescription => '选择您的设备与他人共享的信息。';
@override
String get settings_denyAll => '拒绝所有';
@override
String get settings_allowByContact => '按联系人标志允许';
@override
String get settings_allowAll => '允许全部';
@override
String get settings_telemetryBaseMode => '遥测基础模式';
@override
String get settings_telemetryLocationMode => '遥测位置模式';
@override
String get settings_telemetryEnvironmentMode => '遥测环境模式';
@override
String get settings_advertLocation => '广告位置';
@override
String get settings_advertLocationSubtitle => '在广告中包含位置';
@override
String settings_multiAck(String value) {
return '多重ACK$value';
}
@override
String get settings_telemetryModeUpdated => '遥测模式已更新';
@override @override
String get settings_actions => '操作'; String get settings_actions => '操作';
@@ -845,6 +886,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get contacts_groupNameRequired => '请输入群聊名称'; String get contacts_groupNameRequired => '请输入群聊名称';
@override
String get contacts_groupNameReserved => '该群组名称已被保留';
@override @override
String contacts_groupAlreadyExists(String name) { String contacts_groupAlreadyExists(String name) {
return '名为 \"$name\" 的群聊已存在'; return '名为 \"$name\" 的群聊已存在';
@@ -883,6 +927,39 @@ class AppLocalizationsZh extends AppLocalizations {
return '最后在线 $days 天前'; return '最后在线 $days 天前';
} }
@override
String get contact_info => '联系信息';
@override
String get contact_settings => '联系人设置';
@override
String get contact_telemetry => '遥测数据';
@override
String get contact_lastSeen => '最近出现';
@override
String get contact_clearChat => '清除聊天记录';
@override
String get contact_teleBase => '遥测基站';
@override
String get contact_teleBaseSubtitle => '允许共享电池电量和基本遥测数据';
@override
String get contact_teleLoc => '遥测位置';
@override
String get contact_teleLocSubtitle => '允许共享位置数据';
@override
String get contact_teleEnv => '遥测环境';
@override
String get contact_teleEnvSubtitle => '允许共享环境传感器数据';
@override @override
String get channels_title => '频道'; String get channels_title => '频道';
+33 -1
View File
@@ -285,6 +285,7 @@
"contacts_newGroup": "Nieuwe Groep", "contacts_newGroup": "Nieuwe Groep",
"contacts_groupName": "Groepnaam", "contacts_groupName": "Groepnaam",
"contacts_groupNameRequired": "De groepnaam is verplicht.", "contacts_groupNameRequired": "De groepnaam is verplicht.",
"contacts_groupNameReserved": "Deze groepsnaam is gereserveerd",
"contacts_groupAlreadyExists": "De groep \"{name}\" bestaat al.", "contacts_groupAlreadyExists": "De groep \"{name}\" bestaat al.",
"@contacts_groupAlreadyExists": { "@contacts_groupAlreadyExists": {
"placeholders": { "placeholders": {
@@ -1888,5 +1889,36 @@
"tcpErrorTimedOut": "De TCP-verbinding is verlopen.", "tcpErrorTimedOut": "De TCP-verbinding is verlopen.",
"tcpConnectionFailed": "Verbinding met TCP mislukt: {error}", "tcpConnectionFailed": "Verbinding met TCP mislukt: {error}",
"map_showDiscoveryContacts": "Ontdek contacten weergeven", "map_showDiscoveryContacts": "Ontdek contacten weergeven",
"map_setAsMyLocation": "Stel dit in als mijn locatie" "map_setAsMyLocation": "Stel dit in als mijn locatie",
"settings_privacy": "Privacyinstellingen",
"settings_privacySubtitle": "Beheer welke informatie wordt gedeeld",
"settings_telemetryLocationMode": "Telemetrie-locatiemodus",
"settings_telemetryEnvironmentMode": "Telemetrie-omgevingsmodus",
"settings_advertLocation": "Advertentielocatie",
"settings_advertLocationSubtitle": "Locatie opnemen in advertentie",
"settings_privacySettingsDescription": "Kies welke informatie uw apparaat deelt met anderen",
"settings_allowByContact": "Toestaan op basis van contactvlaggen",
"settings_allowAll": "Alles toestaan",
"settings_denyAll": "Weiger alles",
"contact_info": "Contactinformatie",
"settings_telemetryBaseMode": "Telemetrie-basismodus",
"contact_teleBase": "Telemetrie_basis",
"contact_teleLoc": "Telemetrielocatie",
"contact_teleLocSubtitle": "Locatiegegevens delen toestaan",
"contact_teleEnv": "Telemetrieomgeving",
"contact_teleEnvSubtitle": "Delen van omgevingsensordata toestaan",
"contact_settings": "Contactinstellingen",
"contact_telemetry": "Telemetrie",
"contact_lastSeen": "Laatst gezien",
"contact_clearChat": "Chat leegmaken",
"contact_teleBaseSubtitle": "Sta delen van batterij niveau en basis telemetrie toe",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"settings_telemetryModeUpdated": "Telemetrie-modus bijgewerkt",
"settings_multiAck": "Multi-ACKs: {value}"
} }
+33 -1
View File
@@ -285,6 +285,7 @@
"contacts_newGroup": "Nowa Grupa", "contacts_newGroup": "Nowa Grupa",
"contacts_groupName": "Nazwa grupy", "contacts_groupName": "Nazwa grupy",
"contacts_groupNameRequired": "Nazwa grupy jest wymagana", "contacts_groupNameRequired": "Nazwa grupy jest wymagana",
"contacts_groupNameReserved": "Ta nazwa grupy jest zastrzeżona",
"contacts_groupAlreadyExists": "Grupa \"{name}\" już istnieje", "contacts_groupAlreadyExists": "Grupa \"{name}\" już istnieje",
"@contacts_groupAlreadyExists": { "@contacts_groupAlreadyExists": {
"placeholders": { "placeholders": {
@@ -1888,5 +1889,36 @@
"tcpErrorTimedOut": "Połączenie TCP zakończyło się bez powodzenia.", "tcpErrorTimedOut": "Połączenie TCP zakończyło się bez powodzenia.",
"tcpConnectionFailed": "Błąd połączenia TCP: {error}", "tcpConnectionFailed": "Błąd połączenia TCP: {error}",
"map_showDiscoveryContacts": "Pokaż kontakty odkrywania", "map_showDiscoveryContacts": "Pokaż kontakty odkrywania",
"map_setAsMyLocation": "Ustaw jako moje lokalizację" "map_setAsMyLocation": "Ustaw jako moje lokalizację",
"settings_allowByContact": "Zezwalaj według flag kontaktowych",
"settings_allowAll": "Zezwalaj na wszystko",
"settings_telemetryLocationMode": "Tryb położenia telemetrycznego",
"settings_telemetryEnvironmentMode": "Tryb środowiska telemetrycznego",
"settings_advertLocation": "Lokalizacja reklamowa",
"settings_advertLocationSubtitle": "Uwzględnij lokalizację w ogłoszeniu",
"settings_denyAll": "Odmów wszystkim",
"settings_privacySubtitle": "Kontroluj jakie informacje są udostępniane.",
"settings_privacy": "Ustawienia prywatności",
"settings_privacySettingsDescription": "Wybierz jakie informacje urządzenie udostępni innym.",
"contact_info": "Informacje kontaktowe",
"settings_telemetryBaseMode": "Tryb podstawowy telemetrii",
"contact_teleBase": "Baza telemetryczna",
"contact_teleLoc": "Lokalizacja telemetryczna",
"contact_teleLocSubtitle": "Zezwalaj na udostępnianie danych lokalizacji",
"contact_teleEnv": "Środowisko telemetryczne",
"contact_teleEnvSubtitle": "Zezwalaj na udostępnianie danych czujników środowiskowych",
"contact_telemetry": "Telemetryka",
"contact_clearChat": "Wyczyść czat",
"contact_settings": "Ustawienia kontaktowe",
"contact_lastSeen": "Ostatnio widziany",
"contact_teleBaseSubtitle": "Pozwól na udostępnianie poziomu naładowania baterii i podstawowych danych telemetrycznych",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"settings_telemetryModeUpdated": "Tryb telemetryczny zaktualizowany",
"settings_multiAck": "Wiele potwierdzeń: {value}"
} }
+33 -1
View File
@@ -285,6 +285,7 @@
"contacts_newGroup": "Novo Grupo", "contacts_newGroup": "Novo Grupo",
"contacts_groupName": "Nome do grupo", "contacts_groupName": "Nome do grupo",
"contacts_groupNameRequired": "O nome do grupo é obrigatório.", "contacts_groupNameRequired": "O nome do grupo é obrigatório.",
"contacts_groupNameReserved": "Este nome de grupo está reservado",
"contacts_groupAlreadyExists": "O grupo \"{name}\" já existe", "contacts_groupAlreadyExists": "O grupo \"{name}\" já existe",
"@contacts_groupAlreadyExists": { "@contacts_groupAlreadyExists": {
"placeholders": { "placeholders": {
@@ -1888,5 +1889,36 @@
"tcpErrorTimedOut": "A conexão TCP expirou.", "tcpErrorTimedOut": "A conexão TCP expirou.",
"tcpConnectionFailed": "Falha na conexão TCP: {error}", "tcpConnectionFailed": "Falha na conexão TCP: {error}",
"map_showDiscoveryContacts": "Mostrar Contatos de Descoberta", "map_showDiscoveryContacts": "Mostrar Contatos de Descoberta",
"map_setAsMyLocation": "Defina minha localização" "map_setAsMyLocation": "Defina minha localização",
"settings_privacySettingsDescription": "Escolha quais informações o seu dispositivo compartilha com os outros.",
"settings_allowByContact": "Permitir por bandeiras de contato",
"settings_telemetryLocationMode": "Modo de Localização de Telemetria",
"settings_telemetryEnvironmentMode": "Modo de Ambiente de Telemetria",
"settings_advertLocation": "Localização do Anúncio",
"settings_advertLocationSubtitle": "Incluir localização no anúncio",
"settings_privacySubtitle": "Controle o que é compartilhado.",
"settings_denyAll": "Negar todos",
"settings_allowAll": "Permitir todos",
"settings_privacy": "Configurações de Privacidade",
"contact_info": "Informações de Contato",
"settings_telemetryBaseMode": "Modo Base de Telemetria",
"contact_teleBase": "Base de Telemetria",
"contact_teleLoc": "Localização de Telemetria",
"contact_teleLocSubtitle": "Permitir compartilhamento de dados de localização",
"contact_teleEnv": "Ambiente de Telemetria",
"contact_teleEnvSubtitle": "Permitir compartilhamento de dados do sensor de ambiente",
"contact_lastSeen": "Visto pela última vez",
"contact_clearChat": "Limpar Chat",
"contact_telemetry": "Telemetria",
"contact_settings": "Configurações de Contato",
"contact_teleBaseSubtitle": "Permitir compartilhamento do nível da bateria e telemetria básica",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"settings_telemetryModeUpdated": "Modo de telemetria atualizado",
"settings_multiAck": "Multi-ACKs: {value}"
} }
+33 -1
View File
@@ -212,6 +212,7 @@
"contacts_newGroup": "Новая группа", "contacts_newGroup": "Новая группа",
"contacts_groupName": "Имя группы", "contacts_groupName": "Имя группы",
"contacts_groupNameRequired": "Имя группы обязательно", "contacts_groupNameRequired": "Имя группы обязательно",
"contacts_groupNameReserved": "Это имя группы зарезервировано",
"contacts_groupAlreadyExists": "Группа \"{name}\" уже существует", "contacts_groupAlreadyExists": "Группа \"{name}\" уже существует",
"contacts_filterContacts": "Фильтр контактов...", "contacts_filterContacts": "Фильтр контактов...",
"contacts_noContactsMatchFilter": "Нет контактов, соответствующих фильтру", "contacts_noContactsMatchFilter": "Нет контактов, соответствующих фильтру",
@@ -1128,5 +1129,36 @@
"tcpErrorTimedOut": "Соединение TCP не удалось установить.", "tcpErrorTimedOut": "Соединение TCP не удалось установить.",
"tcpConnectionFailed": "Не удалось установить соединение TCP: {error}", "tcpConnectionFailed": "Не удалось установить соединение TCP: {error}",
"map_showDiscoveryContacts": "Показать контакты Discovery", "map_showDiscoveryContacts": "Показать контакты Discovery",
"map_setAsMyLocation": "Установить мое местоположение" "map_setAsMyLocation": "Установить мое местоположение",
"settings_privacy": "Настройки конфиденциальности",
"settings_privacySubtitle": "Контролируйте, какую информацию делиться.",
"settings_telemetryLocationMode": "Режим местоположения телеметрии",
"settings_telemetryEnvironmentMode": "Режим среды телеметрии",
"settings_advertLocation": "Местоположение рекламы",
"settings_advertLocationSubtitle": "Включить местоположение в объявление",
"settings_allowAll": "Разрешить все",
"settings_privacySettingsDescription": "Выберите, какую информацию ваше устройство будет делиться с другими.",
"settings_denyAll": "Отклонить все",
"settings_allowByContact": "Разрешить по флагам контактов",
"contact_info": "Контактная информация",
"settings_telemetryBaseMode": "Базовый режим телеметрии",
"contact_teleBase": "База телеметрии",
"contact_teleLoc": "Местоположение телеметрии",
"contact_teleLocSubtitle": "Разрешить обмен данными о местоположении",
"contact_teleEnv": "Среда телеметрии",
"contact_teleEnvSubtitle": "Разрешить обмен данными датчиков окружающей среды",
"contact_settings": "Настройки контактов",
"contact_telemetry": "Телеметрия",
"contact_clearChat": "Очистить чат",
"contact_lastSeen": "Последний раз видели",
"contact_teleBaseSubtitle": "Разрешить обмен уровнем заряда батареи и базовой телеметрией",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"settings_telemetryModeUpdated": "Режим телеметрии обновлен",
"settings_multiAck": "Мульти-ACK: {value}"
} }
+33 -1
View File
@@ -285,6 +285,7 @@
"contacts_newGroup": "Nová skupina", "contacts_newGroup": "Nová skupina",
"contacts_groupName": "Názov skupiny", "contacts_groupName": "Názov skupiny",
"contacts_groupNameRequired": "Skupina musí mať názov.", "contacts_groupNameRequired": "Skupina musí mať názov.",
"contacts_groupNameReserved": "Tento názov skupiny je rezervovaný",
"contacts_groupAlreadyExists": "Skupina \"{name}\" už existuje", "contacts_groupAlreadyExists": "Skupina \"{name}\" už existuje",
"@contacts_groupAlreadyExists": { "@contacts_groupAlreadyExists": {
"placeholders": { "placeholders": {
@@ -1888,5 +1889,36 @@
"tcpErrorTimedOut": "Pripojenie TCP vypršalo.", "tcpErrorTimedOut": "Pripojenie TCP vypršalo.",
"tcpConnectionFailed": "Neúspešné vytvorenie TCP spojenia: {error}", "tcpConnectionFailed": "Neúspešné vytvorenie TCP spojenia: {error}",
"map_showDiscoveryContacts": "Zobraziť kontakty objavov", "map_showDiscoveryContacts": "Zobraziť kontakty objavov",
"map_setAsMyLocation": "Nastavte ako moju polohu" "map_setAsMyLocation": "Nastavte ako moju polohu",
"settings_privacy": "Nastavenia súkromia",
"settings_privacySubtitle": "Ovládni, aké informácie sa zdieľajú.",
"settings_telemetryLocationMode": "Režim umiestnenia telemetrie",
"settings_telemetryBaseMode": "Základný režim telemetrie",
"settings_advertLocation": "Umiestnenie inzerátu",
"settings_telemetryEnvironmentMode": "Režim prostredia telemetrie",
"settings_advertLocationSubtitle": "Zahrnúť polohu do inzerátu",
"settings_allowAll": "Povoliť všetko",
"settings_privacySettingsDescription": "Vyberte, ktoré informácie váš zariadenie zdieľa s ostatnými.",
"settings_denyAll": "Zamietnuť všetko",
"settings_allowByContact": "Povoliť podľa kontaktových vlajok",
"contact_info": "Kontaktné informácie",
"contact_settings": "Nastavenia kontaktov",
"contact_teleBaseSubtitle": "Povoliť zdieľanie úrovne batérie a základnej telemetrie",
"contact_teleLoc": "Lokácia telemetrie",
"contact_teleLocSubtitle": "Povoliť zdieľanie údajov o lokalite",
"contact_teleEnv": "Prostredie telemetrie",
"contact_telemetry": "Telemetria",
"contact_clearChat": "Vymazať chat",
"contact_lastSeen": "Naposledy videný",
"contact_teleBase": "Báza telemetrie",
"contact_teleEnvSubtitle": "Povoliť zdieľanie údajov senzorov prostredia",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"settings_telemetryModeUpdated": "Režim telemetrie bol aktualizovaný",
"settings_multiAck": "Viaceré ACK: {value}"
} }
+33 -1
View File
@@ -285,6 +285,7 @@
"contacts_newGroup": "Nova skupina", "contacts_newGroup": "Nova skupina",
"contacts_groupName": "Ime skupine", "contacts_groupName": "Ime skupine",
"contacts_groupNameRequired": "Ime skupine je obvezno.", "contacts_groupNameRequired": "Ime skupine je obvezno.",
"contacts_groupNameReserved": "To ime skupine je rezervirano",
"contacts_groupAlreadyExists": "Skupina \"{name}\" že obstaja", "contacts_groupAlreadyExists": "Skupina \"{name}\" že obstaja",
"@contacts_groupAlreadyExists": { "@contacts_groupAlreadyExists": {
"placeholders": { "placeholders": {
@@ -1888,5 +1889,36 @@
"tcpErrorTimedOut": "Povezava TCP je presegla časovno obdobje.", "tcpErrorTimedOut": "Povezava TCP je presegla časovno obdobje.",
"tcpConnectionFailed": "Napaka pri povezavi TCP: {error}", "tcpConnectionFailed": "Napaka pri povezavi TCP: {error}",
"map_showDiscoveryContacts": "Prikaži odkritja kontaktov", "map_showDiscoveryContacts": "Prikaži odkritja kontaktov",
"map_setAsMyLocation": "Nastavite to kot mojo lokacijo" "map_setAsMyLocation": "Nastavite to kot mojo lokacijo",
"settings_privacy": "Nastavitve zasebnosti",
"settings_privacySettingsDescription": "Izberite, katere informacije vaš naprava deli z drugimi.",
"settings_telemetryBaseMode": "Osnovni način telemetrije",
"settings_telemetryLocationMode": "Način delovanja telemetrije",
"settings_telemetryEnvironmentMode": "Način delovanja okolja telemetrije",
"settings_advertLocation": "Lokacija oglasa",
"settings_allowByContact": "Dovoli po kontaktnih zastavah",
"settings_denyAll": "Zavrniti vse",
"settings_allowAll": "Dovoli vse",
"settings_privacySubtitle": "Kontrolirajte, katere informacije so deljene.",
"contact_info": "Kontaktni podatki",
"contact_teleBase": "Baza telemetrije",
"contact_teleBaseSubtitle": "Dovoli deljenje stanja baterije in osnovne telemetrije",
"contact_teleLoc": "Lokacija telemetrije",
"contact_lastSeen": "Zadnjič videno",
"contact_settings": "Nastavitve stika",
"settings_advertLocationSubtitle": "Vključi lokacijo v oglas.",
"contact_telemetry": "Telemetrija",
"contact_clearChat": "Počisti klepet",
"contact_teleEnv": "Okolje telemetrije",
"contact_teleEnvSubtitle": "Dovoli deljenje podatkov okoljskih senzorjev",
"contact_teleLocSubtitle": "Dovoli deljenje podatkov o lokaciji",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"settings_multiAck": "Večkratni potrditvi: {value}",
"settings_telemetryModeUpdated": "Način telemetrije posodobljen"
} }
+33 -1
View File
@@ -285,6 +285,7 @@
"contacts_newGroup": "Ny grupp", "contacts_newGroup": "Ny grupp",
"contacts_groupName": "Gruppnamn", "contacts_groupName": "Gruppnamn",
"contacts_groupNameRequired": "Gruppnamnet är obligatoriskt", "contacts_groupNameRequired": "Gruppnamnet är obligatoriskt",
"contacts_groupNameReserved": "Detta gruppnamn är reserverat",
"contacts_groupAlreadyExists": "Gruppen \"{name}\" finns redan.", "contacts_groupAlreadyExists": "Gruppen \"{name}\" finns redan.",
"@contacts_groupAlreadyExists": { "@contacts_groupAlreadyExists": {
"placeholders": { "placeholders": {
@@ -1888,5 +1889,36 @@
"tcpErrorTimedOut": "TCP-anslutningen har tidsut gått.", "tcpErrorTimedOut": "TCP-anslutningen har tidsut gått.",
"tcpConnectionFailed": "Fel vid TCP-anslutning: {error}", "tcpConnectionFailed": "Fel vid TCP-anslutning: {error}",
"map_showDiscoveryContacts": "Visa Discovery-kontakter", "map_showDiscoveryContacts": "Visa Discovery-kontakter",
"map_setAsMyLocation": "Ange som min plats" "map_setAsMyLocation": "Ange som min plats",
"settings_privacy": "Inställningar för sekretess",
"settings_allowAll": "Tillåt alla",
"settings_privacySubtitle": "Kontrollera vilken information som delas.",
"settings_telemetryEnvironmentMode": "Telemetri miljöläge",
"settings_telemetryBaseMode": "Telemetribasläge",
"settings_telemetryLocationMode": "Telemetritillstånd för plats",
"settings_advertLocation": "Annonsplacering",
"contact_info": "Kontaktinformation",
"contact_settings": "Kontaktinställningar",
"contact_telemetry": "Telemetri",
"settings_denyAll": "Neka alla",
"settings_allowByContact": "Tillåt via kontaktflaggor",
"settings_privacySettingsDescription": "Välj vilken information din enhet delar med andra.",
"contact_lastSeen": "Senast sedd",
"contact_clearChat": "Rensa Chatt",
"contact_teleEnv": "Telemetri Miljö",
"settings_advertLocationSubtitle": "Inkludera plats i annonsen",
"contact_teleEnvSubtitle": "Tillåt delning av miljösensordata",
"contact_teleBase": "Telemetribas",
"contact_teleBaseSubtitle": "Tillåt delning av batterinivå och grundläggande telemetri",
"contact_teleLoc": "Telemetridata plats",
"contact_teleLocSubtitle": "Tillåt delning av platsdata",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"settings_telemetryModeUpdated": "Telemetri-läge uppdaterat",
"settings_multiAck": "Multi-ACKs: {value}"
} }
+33 -1
View File
@@ -286,6 +286,7 @@
"contacts_newGroup": "Нова група", "contacts_newGroup": "Нова група",
"contacts_groupName": "Назва групи", "contacts_groupName": "Назва групи",
"contacts_groupNameRequired": "Назва групи обов'язкова.", "contacts_groupNameRequired": "Назва групи обов'язкова.",
"contacts_groupNameReserved": "Ця назва групи зарезервована",
"contacts_groupAlreadyExists": "Група «{name}» вже існує.", "contacts_groupAlreadyExists": "Група «{name}» вже існує.",
"@contacts_groupAlreadyExists": { "@contacts_groupAlreadyExists": {
"placeholders": { "placeholders": {
@@ -1888,5 +1889,36 @@
"tcpErrorTimedOut": "З'єднання TCP завершилося через закінчення часу очікування.", "tcpErrorTimedOut": "З'єднання TCP завершилося через закінчення часу очікування.",
"tcpConnectionFailed": "Не вдалося встановити з'єднання TCP: {error}", "tcpConnectionFailed": "Не вдалося встановити з'єднання TCP: {error}",
"map_showDiscoveryContacts": "Показати контакти Відкриття", "map_showDiscoveryContacts": "Показати контакти Відкриття",
"map_setAsMyLocation": "Встановити моє місцезнаходження" "map_setAsMyLocation": "Встановити моє місцезнаходження",
"settings_privacySubtitle": "Керуйте інформацією, яку буде спільно використовуватися",
"settings_privacy": "Налаштування приватності",
"settings_telemetryBaseMode": "Режим базової телеметрії",
"settings_telemetryLocationMode": "Режим місця телеметрії",
"settings_advertLocation": "Розміщення реклами",
"settings_advertLocationSubtitle": "Включити місце розташування в оголошення",
"settings_privacySettingsDescription": "Виберіть, яку інформацію ваш пристрій буде передавати іншим.",
"settings_allowAll": "Дозволити все",
"settings_denyAll": "Відхилити все",
"settings_allowByContact": "Дозволити за контактними прапорцями",
"settings_telemetryEnvironmentMode": "Режим середовища телеметрії",
"contact_info": "Контактна інформація",
"contact_teleBaseSubtitle": "Дозволити спільний доступ до рівня заряду батареї та базової телеметрії",
"contact_teleLoc": "Розташування телеметрії",
"contact_teleBase": "Базовий телебачення",
"contact_teleLocSubtitle": "Дозволити спільне використання даних про місцеположення",
"contact_settings": "Налаштування контактів",
"contact_telemetry": "Телеметрія",
"contact_clearChat": "Очистити чат",
"contact_lastSeen": "Останній раз бачили",
"contact_teleEnv": "Середовище телеметрії",
"contact_teleEnvSubtitle": "Дозволити спільний доступ до даних датчиків середовища",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"settings_telemetryModeUpdated": "Режим телеметрії оновлено",
"settings_multiAck": "Багатократне підтвердження: {value}"
} }
+33 -1
View File
@@ -300,6 +300,7 @@
"contacts_newGroup": "新建群聊", "contacts_newGroup": "新建群聊",
"contacts_groupName": "群聊名称", "contacts_groupName": "群聊名称",
"contacts_groupNameRequired": "请输入群聊名称", "contacts_groupNameRequired": "请输入群聊名称",
"contacts_groupNameReserved": "该群组名称已被保留",
"contacts_groupAlreadyExists": "名为 \"{name}\" 的群聊已存在", "contacts_groupAlreadyExists": "名为 \"{name}\" 的群聊已存在",
"@contacts_groupAlreadyExists": { "@contacts_groupAlreadyExists": {
"placeholders": { "placeholders": {
@@ -1893,5 +1894,36 @@
"tcpErrorTimedOut": "TCP 连接超时。", "tcpErrorTimedOut": "TCP 连接超时。",
"tcpConnectionFailed": "TCP 连接失败:{error}", "tcpConnectionFailed": "TCP 连接失败:{error}",
"map_showDiscoveryContacts": "显示发现联系人", "map_showDiscoveryContacts": "显示发现联系人",
"map_setAsMyLocation": "设置为我的位置" "map_setAsMyLocation": "设置为我的位置",
"settings_privacySubtitle": "控制要共享的信息。",
"settings_privacySettingsDescription": "选择您的设备与他人共享的信息。",
"settings_telemetryBaseMode": "遥测基础模式",
"settings_telemetryLocationMode": "遥测位置模式",
"settings_advertLocation": "广告位置",
"settings_advertLocationSubtitle": "在广告中包含位置",
"settings_allowByContact": "按联系人标志允许",
"settings_denyAll": "拒绝所有",
"settings_privacy": "隐私设置",
"settings_allowAll": "允许全部",
"contact_info": "联系信息",
"contact_teleBase": "遥测基站",
"contact_teleBaseSubtitle": "允许共享电池电量和基本遥测数据",
"settings_telemetryEnvironmentMode": "遥测环境模式",
"contact_teleLoc": "遥测位置",
"contact_teleEnv": "遥测环境",
"contact_teleEnvSubtitle": "允许共享环境传感器数据",
"contact_clearChat": "清除聊天记录",
"contact_lastSeen": "最近出现",
"contact_settings": "联系人设置",
"contact_teleLocSubtitle": "允许共享位置数据",
"contact_telemetry": "遥测数据",
"@settings_multiAck": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"settings_multiAck": "多重ACK{value}",
"settings_telemetryModeUpdated": "遥测模式已更新"
} }
+7
View File
@@ -19,6 +19,7 @@ import 'services/app_debug_log_service.dart';
import 'services/background_service.dart'; import 'services/background_service.dart';
import 'services/map_tile_cache_service.dart'; import 'services/map_tile_cache_service.dart';
import 'services/chat_text_scale_service.dart'; import 'services/chat_text_scale_service.dart';
import 'services/ui_view_state_service.dart';
import 'services/timeout_prediction_service.dart'; import 'services/timeout_prediction_service.dart';
import 'storage/prefs_manager.dart'; import 'storage/prefs_manager.dart';
import 'utils/app_logger.dart'; import 'utils/app_logger.dart';
@@ -40,6 +41,7 @@ void main() async {
final backgroundService = BackgroundService(); final backgroundService = BackgroundService();
final mapTileCacheService = MapTileCacheService(); final mapTileCacheService = MapTileCacheService();
final chatTextScaleService = ChatTextScaleService(); final chatTextScaleService = ChatTextScaleService();
final uiViewStateService = UiViewStateService();
final timeoutPredictionService = TimeoutPredictionService(storage); final timeoutPredictionService = TimeoutPredictionService(storage);
// Load settings // Load settings
@@ -58,6 +60,7 @@ void main() async {
_registerThirdPartyLicenses(); _registerThirdPartyLicenses();
await chatTextScaleService.initialize(); await chatTextScaleService.initialize();
await uiViewStateService.initialize();
await timeoutPredictionService.initialize(); await timeoutPredictionService.initialize();
// Wire up connector with services // Wire up connector with services
@@ -90,6 +93,7 @@ void main() async {
appDebugLogService: appDebugLogService, appDebugLogService: appDebugLogService,
mapTileCacheService: mapTileCacheService, mapTileCacheService: mapTileCacheService,
chatTextScaleService: chatTextScaleService, chatTextScaleService: chatTextScaleService,
uiViewStateService: uiViewStateService,
timeoutPredictionService: timeoutPredictionService, timeoutPredictionService: timeoutPredictionService,
), ),
); );
@@ -126,6 +130,7 @@ class MeshCoreApp extends StatelessWidget {
final AppDebugLogService appDebugLogService; final AppDebugLogService appDebugLogService;
final MapTileCacheService mapTileCacheService; final MapTileCacheService mapTileCacheService;
final ChatTextScaleService chatTextScaleService; final ChatTextScaleService chatTextScaleService;
final UiViewStateService uiViewStateService;
final TimeoutPredictionService timeoutPredictionService; final TimeoutPredictionService timeoutPredictionService;
const MeshCoreApp({ const MeshCoreApp({
@@ -139,6 +144,7 @@ class MeshCoreApp extends StatelessWidget {
required this.appDebugLogService, required this.appDebugLogService,
required this.mapTileCacheService, required this.mapTileCacheService,
required this.chatTextScaleService, required this.chatTextScaleService,
required this.uiViewStateService,
required this.timeoutPredictionService, required this.timeoutPredictionService,
}); });
@@ -153,6 +159,7 @@ class MeshCoreApp extends StatelessWidget {
ChangeNotifierProvider.value(value: bleDebugLogService), ChangeNotifierProvider.value(value: bleDebugLogService),
ChangeNotifierProvider.value(value: appDebugLogService), ChangeNotifierProvider.value(value: appDebugLogService),
ChangeNotifierProvider.value(value: chatTextScaleService), ChangeNotifierProvider.value(value: chatTextScaleService),
ChangeNotifierProvider.value(value: uiViewStateService),
Provider.value(value: storage), Provider.value(value: storage),
Provider.value(value: mapTileCacheService), Provider.value(value: mapTileCacheService),
ChangeNotifierProvider.value(value: timeoutPredictionService), ChangeNotifierProvider.value(value: timeoutPredictionService),
+12
View File
@@ -40,6 +40,8 @@ class AppSettings {
final UnitSystem unitSystem; final UnitSystem unitSystem;
final Set<String> mutedChannels; final Set<String> mutedChannels;
final bool mapShowDiscoveryContacts; final bool mapShowDiscoveryContacts;
final String tcpServerAddress;
final int tcpServerPort;
AppSettings({ AppSettings({
this.clearPathOnMaxRetry = false, this.clearPathOnMaxRetry = false,
@@ -68,6 +70,8 @@ class AppSettings {
this.unitSystem = UnitSystem.metric, this.unitSystem = UnitSystem.metric,
Set<String>? mutedChannels, Set<String>? mutedChannels,
this.mapShowDiscoveryContacts = true, this.mapShowDiscoveryContacts = true,
this.tcpServerAddress = '',
this.tcpServerPort = 0,
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {}, }) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {}, batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
mutedChannels = mutedChannels ?? {}; mutedChannels = mutedChannels ?? {};
@@ -100,6 +104,8 @@ class AppSettings {
'unit_system': unitSystem.value, 'unit_system': unitSystem.value,
'muted_channels': mutedChannels.toList(), 'muted_channels': mutedChannels.toList(),
'map_show_discovery_contacts': mapShowDiscoveryContacts, 'map_show_discovery_contacts': mapShowDiscoveryContacts,
'tcp_server_address': tcpServerAddress,
'tcp_server_port': tcpServerPort,
}; };
} }
@@ -157,6 +163,8 @@ class AppSettings {
{}, {},
mapShowDiscoveryContacts: mapShowDiscoveryContacts:
json['map_show_discovery_contacts'] as bool? ?? true, json['map_show_discovery_contacts'] as bool? ?? true,
tcpServerAddress: json['tcp_server_address'] as String? ?? '',
tcpServerPort: json['tcp_server_port'] as int? ?? 0,
); );
} }
@@ -187,6 +195,8 @@ class AppSettings {
UnitSystem? unitSystem, UnitSystem? unitSystem,
Set<String>? mutedChannels, Set<String>? mutedChannels,
bool? mapShowDiscoveryContacts, bool? mapShowDiscoveryContacts,
String? tcpServerAddress,
int? tcpServerPort,
}) { }) {
return AppSettings( return AppSettings(
clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry, clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry,
@@ -225,6 +235,8 @@ class AppSettings {
mutedChannels: mutedChannels ?? this.mutedChannels, mutedChannels: mutedChannels ?? this.mutedChannels,
mapShowDiscoveryContacts: mapShowDiscoveryContacts:
mapShowDiscoveryContacts ?? this.mapShowDiscoveryContacts, mapShowDiscoveryContacts ?? this.mapShowDiscoveryContacts,
tcpServerAddress: tcpServerAddress ?? this.tcpServerAddress,
tcpServerPort: tcpServerPort ?? this.tcpServerPort,
); );
} }
} }
+17 -39
View File
@@ -65,7 +65,17 @@ class Contact {
return '$pathLength hops'; return '$pathLength hops';
} }
bool get hasLocation => latitude != null && longitude != null; bool get hasLocation {
const double epsilon = 1e-6;
final lat = latitude ?? 0.0;
final lon = longitude ?? 0.0;
return (lat.abs() > epsilon || lon.abs() > epsilon) &&
lat >= -90.0 &&
lat <= 90.0 &&
lon >= -180.0 &&
lon <= 180.0;
}
bool get isFavorite => (flags & contactFlagFavorite) != 0; bool get isFavorite => (flags & contactFlagFavorite) != 0;
Contact copyWith({ Contact copyWith({
@@ -108,7 +118,7 @@ class Contact {
} }
String get pathIdList { String get pathIdList {
final pathBytes = _pathBytesForDisplay; final pathBytes = pathBytesForDisplay;
if (pathBytes.isEmpty) return ''; if (pathBytes.isEmpty) return '';
final parts = <String>[]; final parts = <String>[];
final groupSize = pathHashSize; final groupSize = pathHashSize;
@@ -130,43 +140,7 @@ class Contact {
return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>"; return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>";
} }
Uint8List? get traceRouteBytes { Uint8List get pathBytesForDisplay {
final pathBytes = _pathBytesForDisplay;
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;
}
Uint8List get _pathBytesForDisplay {
if (pathOverride != null) { if (pathOverride != null) {
if (pathOverride! < 0) return Uint8List(0); if (pathOverride! < 0) return Uint8List(0);
return pathOverrideBytes ?? Uint8List(0); return pathOverrideBytes ?? Uint8List(0);
@@ -197,6 +171,7 @@ class Contact {
double? lat, lon; double? lat, lon;
final latRaw = reader.readInt32LE(); final latRaw = reader.readInt32LE();
final lonRaw = reader.readInt32LE(); final lonRaw = reader.readInt32LE();
if (latRaw != 0 || lonRaw != 0) { if (latRaw != 0 || lonRaw != 0) {
lat = latRaw / 1e6; lat = latRaw / 1e6;
lon = lonRaw / 1e6; lon = lonRaw / 1e6;
@@ -227,4 +202,7 @@ class Contact {
@override @override
int get hashCode => publicKeyHex.hashCode; int get hashCode => publicKeyHex.hashCode;
bool get teleBaseEnabled => (flags & contactFlagTeleBase) != 0;
bool get teleLocEnabled => (flags & contactFlagTeleLoc) != 0;
bool get teleEnvEnabled => (flags & contactFlagTeleEnv) != 0;
} }
+27
View File
@@ -166,6 +166,33 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
], ],
), ),
centerTitle: false, centerTitle: false,
actions: [
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
onSelected: (value) {
if (value == 'clearChat') {
context.read<MeshCoreConnector>().clearMessagesForChannel(
widget.channel.index,
);
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'clearChat',
child: Row(
children: [
const Icon(Icons.delete, size: 20, color: Colors.red),
const SizedBox(width: 12),
Text(
context.l10n.contact_clearChat,
style: const TextStyle(color: Colors.red),
),
],
),
),
],
),
],
), ),
body: SafeArea( body: SafeArea(
top: false, top: false,
+5 -10
View File
@@ -40,10 +40,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
final primaryPath = !channelMessage && !message.isOutgoing final primaryPath = !channelMessage && !message.isOutgoing
? Uint8List.fromList(primaryPathTmp.reversed.toList()) ? Uint8List.fromList(primaryPathTmp.reversed.toList())
: primaryPathTmp; : primaryPathTmp;
final contacts = <Contact>[ final contacts = connector.allContacts;
...connector.contacts,
...connector.discoveredContacts,
];
final hops = _buildPathHops(primaryPath, contacts, l10n); final hops = _buildPathHops(primaryPath, contacts, l10n);
final hasHopDetails = primaryPath.isNotEmpty; final hasHopDetails = primaryPath.isNotEmpty;
final observedLabel = _formatObservedHops( final observedLabel = _formatObservedHops(
@@ -65,8 +62,9 @@ class ChannelMessagePathScreen extends StatelessWidget {
builder: (context) => PathTraceMapScreen( builder: (context) => PathTraceMapScreen(
title: context.l10n.contacts_repeaterPathTrace, title: context.l10n.contacts_repeaterPathTrace,
path: primaryPath, path: primaryPath,
flipPathRound: true, flipPathAround: true,
reversePathRound: !message.isOutgoing && !channelMessage, reversePathAround:
!(!channelMessage && !message.isOutgoing),
), ),
), ),
), ),
@@ -367,10 +365,7 @@ class _ChannelMessagePathMapScreenState
: selectedPathTmp; : selectedPathTmp;
final selectedIndex = _indexForPath(selectedPath, observedPaths); final selectedIndex = _indexForPath(selectedPath, observedPaths);
final contacts = <Contact>[ final contacts = connector.allContacts;
...connector.contacts,
...connector.discoveredContacts,
];
final hops = _buildPathHops(selectedPath, contacts, context.l10n); final hops = _buildPathHops(selectedPath, contacts, context.l10n);
final points = <LatLng>[]; final points = <LatLng>[];
+44 -55
View File
@@ -11,6 +11,7 @@ import 'package:uuid/uuid.dart';
import '../connector/meshcore_connector.dart'; import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../services/app_settings_service.dart'; import '../services/app_settings_service.dart';
import '../services/ui_view_state_service.dart';
import '../models/channel.dart'; import '../models/channel.dart';
import '../models/community.dart'; import '../models/community.dart';
import '../storage/community_store.dart'; import '../storage/community_store.dart';
@@ -28,8 +29,6 @@ import 'contacts_screen.dart';
import 'map_screen.dart'; import 'map_screen.dart';
import 'settings_screen.dart'; import 'settings_screen.dart';
enum ChannelSortOption { manual, name, latestMessages, unread }
class ChannelsScreen extends StatefulWidget { class ChannelsScreen extends StatefulWidget {
final bool hideBackButton; final bool hideBackButton;
@@ -43,9 +42,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
with DisconnectNavigationMixin { with DisconnectNavigationMixin {
final TextEditingController _searchController = TextEditingController(); final TextEditingController _searchController = TextEditingController();
final CommunityStore _communityStore = CommunityStore(); final CommunityStore _communityStore = CommunityStore();
String _searchQuery = '';
Timer? _searchDebounce; Timer? _searchDebounce;
ChannelSortOption _sortOption = ChannelSortOption.manual;
List<Community> _communities = []; List<Community> _communities = [];
// Cache of PSK hex -> Community for quick lookup // Cache of PSK hex -> Community for quick lookup
@@ -56,6 +53,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_searchController.text = context
.read<UiViewStateService>()
.channelsSearchText;
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<MeshCoreConnector>().getChannels(); context.read<MeshCoreConnector>().getChannels();
_loadCommunities(); _loadCommunities();
@@ -110,6 +110,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final connector = context.watch<MeshCoreConnector>(); final connector = context.watch<MeshCoreConnector>();
final viewState = context.watch<UiViewStateService>();
final channelMessageStore = ChannelMessageStore(); final channelMessageStore = ChannelMessageStore();
channelMessageStore.setPublicKeyHex = connector.selfPublicKeyHex; channelMessageStore.setPublicKeyHex = connector.selfPublicKeyHex;
@@ -205,6 +206,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
final filteredChannels = _filterAndSortChannels( final filteredChannels = _filterAndSortChannels(
channels, channels,
connector, connector,
viewState,
); );
return Column( return Column(
@@ -219,17 +221,19 @@ class _ChannelsScreenState extends State<ChannelsScreen>
suffixIcon: Row( suffixIcon: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (_searchQuery.isNotEmpty) if (viewState.channelsSearchText.isNotEmpty)
IconButton( IconButton(
icon: const Icon(Icons.clear), icon: const Icon(Icons.clear),
onPressed: () { onPressed: () {
_searchDebounce?.cancel();
_searchDebounce = null;
_searchController.clear(); _searchController.clear();
setState(() { context
_searchQuery = ''; .read<UiViewStateService>()
}); .setChannelsSearchText('');
}, },
), ),
_buildFilterButton(), _buildFilterButton(viewState),
], ],
), ),
border: OutlineInputBorder( border: OutlineInputBorder(
@@ -246,9 +250,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
const Duration(milliseconds: 300), const Duration(milliseconds: 300),
() { () {
if (!mounted) return; if (!mounted) return;
setState(() { context
_searchQuery = value.toLowerCase(); .read<UiViewStateService>()
}); .setChannelsSearchText(value);
}, },
); );
}, },
@@ -283,8 +287,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
), ),
], ],
) )
: (_sortOption == ChannelSortOption.manual && : (viewState.channelsSortOption ==
_searchQuery.isEmpty) ChannelSortOption.manual &&
viewState.channelsSearchText.isEmpty)
? ReorderableListView.builder( ? ReorderableListView.builder(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 16, left: 16,
@@ -584,59 +589,40 @@ class _ChannelsScreenState extends State<ChannelsScreen>
await showDisconnectDialog(context, connector); await showDisconnectDialog(context, connector);
} }
Widget _buildFilterButton() { Widget _buildFilterButton(UiViewStateService viewState) {
const actionSortManual = 0; return SortFilterMenu<ChannelSortOption>(
const actionSortName = 1;
const actionSortLatest = 2;
const actionSortUnread = 3;
return SortFilterMenu(
tooltip: context.l10n.listFilter_tooltip, tooltip: context.l10n.listFilter_tooltip,
sections: [ sections: [
SortFilterMenuSection( SortFilterMenuSection<ChannelSortOption>(
title: context.l10n.channels_sortBy, title: context.l10n.channels_sortBy,
options: [ options: [
SortFilterMenuOption( SortFilterMenuOption<ChannelSortOption>(
value: actionSortManual, value: ChannelSortOption.manual,
label: context.l10n.channels_sortManual, label: context.l10n.channels_sortManual,
checked: _sortOption == ChannelSortOption.manual, checked: viewState.channelsSortOption == ChannelSortOption.manual,
), ),
SortFilterMenuOption( SortFilterMenuOption<ChannelSortOption>(
value: actionSortName, value: ChannelSortOption.name,
label: context.l10n.channels_sortAZ, label: context.l10n.channels_sortAZ,
checked: _sortOption == ChannelSortOption.name, checked: viewState.channelsSortOption == ChannelSortOption.name,
), ),
SortFilterMenuOption( SortFilterMenuOption<ChannelSortOption>(
value: actionSortLatest, value: ChannelSortOption.latestMessages,
label: context.l10n.channels_sortLatestMessages, label: context.l10n.channels_sortLatestMessages,
checked: _sortOption == ChannelSortOption.latestMessages, checked:
viewState.channelsSortOption ==
ChannelSortOption.latestMessages,
), ),
SortFilterMenuOption( SortFilterMenuOption<ChannelSortOption>(
value: actionSortUnread, value: ChannelSortOption.unread,
label: context.l10n.channels_sortUnread, label: context.l10n.channels_sortUnread,
checked: _sortOption == ChannelSortOption.unread, checked: viewState.channelsSortOption == ChannelSortOption.unread,
), ),
], ],
), ),
], ],
onSelected: (action) { onSelected: (sortOption) {
setState(() { viewState.setChannelsSortOption(sortOption);
switch (action) {
case actionSortManual:
_sortOption = ChannelSortOption.manual;
break;
case actionSortLatest:
_sortOption = ChannelSortOption.latestMessages;
break;
case actionSortUnread:
_sortOption = ChannelSortOption.unread;
break;
case actionSortName:
default:
_sortOption = ChannelSortOption.name;
break;
}
});
}, },
); );
} }
@@ -644,11 +630,14 @@ class _ChannelsScreenState extends State<ChannelsScreen>
List<Channel> _filterAndSortChannels( List<Channel> _filterAndSortChannels(
List<Channel> channels, List<Channel> channels,
MeshCoreConnector connector, MeshCoreConnector connector,
UiViewStateService viewState,
) { ) {
var filtered = channels.where((channel) { var filtered = channels.where((channel) {
if (_searchQuery.isEmpty) return true; if (viewState.channelsSearchText.isEmpty) return true;
final label = _normalizeChannelName(channel); final label = _normalizeChannelName(channel);
return label.toLowerCase().contains(_searchQuery); return label.toLowerCase().contains(
viewState.channelsSearchText.toLowerCase(),
);
}).toList(); }).toList();
int compareByName(Channel a, Channel b) { int compareByName(Channel a, Channel b) {
@@ -657,7 +646,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
return nameA.toLowerCase().compareTo(nameB.toLowerCase()); return nameA.toLowerCase().compareTo(nameB.toLowerCase());
} }
switch (_sortOption) { switch (viewState.channelsSortOption) {
case ChannelSortOption.manual: case ChannelSortOption.manual:
break; break;
case ChannelSortOption.latestMessages: case ChannelSortOption.latestMessages:
+225 -57
View File
@@ -36,6 +36,7 @@ import '../widgets/gif_picker.dart';
import '../widgets/path_selection_dialog.dart'; import '../widgets/path_selection_dialog.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import 'telemetry_screen.dart';
class ChatScreen extends StatefulWidget { class ChatScreen extends StatefulWidget {
final Contact contact; final Contact contact;
@@ -244,9 +245,77 @@ class _ChatScreenState extends State<ChatScreen> {
tooltip: context.l10n.chat_pathManagement, tooltip: context.l10n.chat_pathManagement,
onPressed: () => _showPathHistory(context), onPressed: () => _showPathHistory(context),
), ),
IconButton( Consumer<MeshCoreConnector>(
icon: const Icon(Icons.info_outline), builder: (context, connector, _) {
onPressed: () => _showContactInfo(context), return PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
onSelected: (value) {
if (value == 'info') {
_showContactInfo(context);
}
if (value == 'settings') {
_showContactSettings(context);
}
if (value == 'telemetry') {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
TelemetryScreen(contact: widget.contact),
),
);
}
if (value == 'clearChat') {
connector.clearMessagesForContact(widget.contact);
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'info',
child: Row(
children: [
const Icon(Icons.info_outline, size: 20),
const SizedBox(width: 12),
Text(context.l10n.contact_info),
],
),
),
PopupMenuItem(
value: 'telemetry',
child: Row(
children: [
const Icon(Icons.bar_chart, size: 20),
const SizedBox(width: 12),
Text(context.l10n.contact_telemetry),
],
),
),
PopupMenuItem(
value: 'settings',
child: Row(
children: [
const Icon(Icons.settings, size: 20),
const SizedBox(width: 12),
Text(context.l10n.contact_settings),
],
),
),
PopupMenuItem(
value: 'clearChat',
child: Row(
children: [
const Icon(Icons.delete, size: 20, color: Colors.red),
const SizedBox(width: 12),
Text(
context.l10n.contact_clearChat,
style: const TextStyle(color: Colors.red),
),
],
),
),
],
);
},
), ),
], ],
), ),
@@ -858,7 +927,7 @@ class _ChatScreenState extends State<ChatScreen> {
builder: (context) => PathTraceMapScreen( builder: (context) => PathTraceMapScreen(
title: context.l10n.contacts_repeaterPathTrace, title: context.l10n.contacts_repeaterPathTrace,
path: Uint8List.fromList(pathBytes), path: Uint8List.fromList(pathBytes),
flipPathRound: true, flipPathAround: true,
targetContact: widget.contact, targetContact: widget.contact,
), ),
), ),
@@ -874,11 +943,22 @@ class _ChatScreenState extends State<ChatScreen> {
); );
} }
int _resolveContactIndex = -1;
Contact _resolveContact(MeshCoreConnector connector) { Contact _resolveContact(MeshCoreConnector connector) {
return connector.contacts.firstWhere( if (_resolveContactIndex >= 0 &&
_resolveContactIndex < connector.contacts.length &&
connector.contacts[_resolveContactIndex].publicKeyHex ==
widget.contact.publicKeyHex) {
return connector.contacts[_resolveContactIndex];
}
_resolveContactIndex = connector.contacts.indexWhere(
(c) => c.publicKeyHex == widget.contact.publicKeyHex, (c) => c.publicKeyHex == widget.contact.publicKeyHex,
orElse: () => widget.contact,
); );
if (_resolveContactIndex == -1) {
return widget.contact;
}
return connector.contacts[_resolveContactIndex];
} }
Contact _resolveContactFrom4Bytes( Contact _resolveContactFrom4Bytes(
@@ -931,59 +1011,127 @@ class _ChatScreenState extends State<ChatScreen> {
void _showContactInfo(BuildContext context) { void _showContactInfo(BuildContext context) {
final connector = Provider.of<MeshCoreConnector>(context, listen: false); final connector = Provider.of<MeshCoreConnector>(context, listen: false);
connector.ensureContactSmazSettingLoaded(widget.contact.publicKeyHex); final contact = _resolveContact(connector);
showDialog( showDialog(
context: context, context: context,
builder: (context) => Consumer<MeshCoreConnector>( builder: (context) => AlertDialog(
builder: (context, connector, _) { title: SelectableText(contact.name),
final contact = _resolveContact(connector); content: SingleChildScrollView(
final smazEnabled = connector.isContactSmazEnabled( child: Column(
contact.publicKeyHex, mainAxisSize: MainAxisSize.min,
); crossAxisAlignment: CrossAxisAlignment.start,
children: [
return AlertDialog( _buildInfoRow(context.l10n.chat_type, contact.typeLabel),
title: Text(contact.name), _buildInfoRow(context.l10n.chat_path, contact.pathLabel),
content: SingleChildScrollView( _buildInfoRow(
child: Column( context.l10n.contact_lastSeen,
mainAxisSize: MainAxisSize.min, _formatContactLastMessage(contact.lastMessageAt),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(context.l10n.chat_type, contact.typeLabel),
_buildInfoRow(context.l10n.chat_path, contact.pathLabel),
if (contact.hasLocation)
_buildInfoRow(
context.l10n.chat_location,
'${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}',
),
_buildInfoRow(
context.l10n.chat_publicKey,
'${contact.publicKeyHex.substring(0, 16)}...',
),
const Divider(),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(context.l10n.channels_smazCompression),
subtitle: Text(context.l10n.chat_compressOutgoingMessages),
value: smazEnabled,
onChanged: (value) {
connector.setContactSmazEnabled(
contact.publicKeyHex,
value,
);
},
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(context.l10n.common_close),
), ),
if (contact.hasLocation)
_buildInfoRow(
context.l10n.chat_location,
'${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}',
),
_buildInfoRow(context.l10n.chat_publicKey, contact.publicKeyHex),
], ],
); ),
}, ),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(context.l10n.common_close),
),
],
),
);
}
void _showContactSettings(BuildContext context) {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
connector.ensureContactSmazSettingLoaded(widget.contact.publicKeyHex);
final contact = widget.contact;
bool smazEnabled = connector.isContactSmazEnabled(contact.publicKeyHex);
bool teleBaseEnabled = contact.teleBaseEnabled;
bool teleLocEnabled = contact.teleLocEnabled;
bool teleEnvEnabled = contact.teleEnvEnabled;
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: Text(context.l10n.contact_settings),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (contact.hasLocation) ...[
_buildInfoRow(
context.l10n.chat_location,
'${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}',
),
const Divider(height: 8),
],
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(context.l10n.channels_smazCompression),
subtitle: Text(context.l10n.chat_compressOutgoingMessages),
value: smazEnabled,
onChanged: (value) {
connector.setContactSmazEnabled(
contact.publicKeyHex,
value,
);
setDialogState(() => smazEnabled = value);
},
),
const Divider(height: 8),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(context.l10n.contact_teleBase),
subtitle: Text(context.l10n.contact_teleBaseSubtitle),
value: teleBaseEnabled,
onChanged: (value) {
setDialogState(() => teleBaseEnabled = value);
},
),
const Divider(height: 8),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(context.l10n.contact_teleLoc),
subtitle: Text(context.l10n.contact_teleLocSubtitle),
value: teleLocEnabled,
onChanged: (value) {
setDialogState(() => teleLocEnabled = value);
},
),
const Divider(height: 8),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(context.l10n.contact_teleEnv),
subtitle: Text(context.l10n.contact_teleEnvSubtitle),
value: teleEnvEnabled,
onChanged: (value) {
setDialogState(() => teleEnvEnabled = value);
},
),
],
),
),
actions: [
TextButton(
onPressed: () {
connector.setContactFlags(
contact,
teleBase: teleBaseEnabled,
teleLoc: teleLocEnabled,
teleEnv: teleEnvEnabled,
);
Navigator.pop(context);
},
child: Text(context.l10n.common_close),
),
],
),
), ),
); );
} }
@@ -998,12 +1146,32 @@ class _ChatScreenState extends State<ChatScreen> {
width: 80, width: 80,
child: Text(label, style: TextStyle(color: Colors.grey[600])), child: Text(label, style: TextStyle(color: Colors.grey[600])),
), ),
Expanded(child: Text(value)), Expanded(child: SelectableText(value)),
], ],
), ),
); );
} }
String _formatContactLastMessage(DateTime timestamp) {
final diff = DateTime.now().difference(timestamp);
if (diff.isNegative || diff.inMinutes < 5) {
return context.l10n.contacts_lastSeenNow;
}
if (diff.inMinutes < 60) {
return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes);
}
if (diff.inHours < 24) {
final hours = diff.inHours;
return hours == 1
? context.l10n.contacts_lastSeenHourAgo
: context.l10n.contacts_lastSeenHoursAgo(hours);
}
final days = diff.inDays;
return days == 1
? context.l10n.contacts_lastSeenDayAgo
: context.l10n.contacts_lastSeenDaysAgo(days);
}
void _openChat(BuildContext context, Contact contact) { void _openChat(BuildContext context, Contact contact) {
// Check if this is a repeater // Check if this is a repeater
context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex); context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex);
@@ -1027,7 +1195,7 @@ class _ChatScreenState extends State<ChatScreen> {
final currentPathLabel = _currentPathLabel(currentContact); final currentPathLabel = _currentPathLabel(currentContact);
// Filter out the current contact from available contacts // Filter out the current contact from available contacts
final availableContacts = connector.contacts final availableContacts = connector.allContacts
.where((c) => c != widget.contact) .where((c) => c != widget.contact)
.toList(); .toList();
File diff suppressed because it is too large Load Diff
+3 -13
View File
@@ -137,10 +137,7 @@ class _MapScreenState extends State<MapScreen> {
builder: (context, connector, settingsService, pathHistory, child) { builder: (context, connector, settingsService, pathHistory, child) {
final tileCache = context.read<MapTileCacheService>(); final tileCache = context.read<MapTileCacheService>();
final settings = settingsService.settings; final settings = settingsService.settings;
final allContacts = <Contact>[ final allContacts = connector.allContacts;
...connector.contacts,
...connector.discoveredContacts.where((c) => !c.isActive),
];
final contacts = settings.mapShowDiscoveryContacts final contacts = settings.mapShowDiscoveryContacts
? allContacts ? allContacts
@@ -179,20 +176,13 @@ class _MapScreenState extends State<MapScreen> {
// Filter by location // Filter by location
final contactsWithLocation = filteredByKeyPrefix.where((c) { final contactsWithLocation = filteredByKeyPrefix.where((c) {
if (!c.hasLocation) { return c.hasLocation;
return false;
}
return _checkLocationPlausibility(c.latitude!, c.longitude!);
}).toList(); }).toList();
// All contacts with a known location used as anchors regardless of // All contacts with a known location used as anchors regardless of
// time/key-prefix filters so that repeaters are always available. // time/key-prefix filters so that repeaters are always available.
final allContactsWithLocation = allContacts final allContactsWithLocation = allContacts
.where( .where((c) => c.hasLocation)
(c) =>
c.hasLocation &&
_checkLocationPlausibility(c.latitude!, c.longitude!),
)
.toList(); .toList();
// Compute guessed locations with caching // Compute guessed locations with caching
+19 -11
View File
@@ -44,6 +44,24 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
PathSelection? _pendingStatusSelection; PathSelection? _pendingStatusSelection;
List<Map<String, dynamic>>? _parsedNeighbors; List<Map<String, dynamic>>? _parsedNeighbors;
int _resolveRepeaterIndex = -1;
Contact _resolveRepeater(MeshCoreConnector connector) {
if (_resolveRepeaterIndex >= 0 &&
_resolveRepeaterIndex < connector.contacts.length &&
connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
widget.repeater.publicKeyHex) {
return connector.contacts[_resolveRepeaterIndex];
}
_resolveRepeaterIndex = connector.contacts.indexWhere(
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
);
if (_resolveRepeaterIndex == -1) {
return widget.repeater;
}
return connector.contacts[_resolveRepeaterIndex];
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -124,10 +142,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) { void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) {
final buffer = BufferReader(frame); final buffer = BufferReader(frame);
final contacts = <Contact>[ final contacts = connector.allContacts;
...connector.contacts,
...connector.discoveredContacts,
];
try { try {
final neighborCount = buffer.readUInt16LE(); final neighborCount = buffer.readUInt16LE();
final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE()); final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE());
@@ -166,13 +181,6 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
} }
} }
Contact _resolveRepeater(MeshCoreConnector connector) {
return connector.contacts.firstWhere(
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
orElse: () => widget.repeater,
);
}
Future<void> _loadNeighbors() async { Future<void> _loadNeighbors() async {
if (_commandService == null) return; if (_commandService == null) return;
+78 -33
View File
@@ -52,8 +52,8 @@ class PathTraceMapScreen extends StatefulWidget {
final String title; final String title;
final Uint8List path; final Uint8List path;
final int? repeaterId; final int? repeaterId;
final bool flipPathRound; final bool flipPathAround;
final bool reversePathRound; final bool reversePathAround;
final Contact? targetContact; final Contact? targetContact;
const PathTraceMapScreen({ const PathTraceMapScreen({
@@ -61,8 +61,8 @@ class PathTraceMapScreen extends StatefulWidget {
required this.title, required this.title,
required this.path, required this.path,
this.repeaterId, this.repeaterId,
this.flipPathRound = false, this.flipPathAround = false,
this.reversePathRound = false, this.reversePathAround = false,
this.targetContact, this.targetContact,
}); });
@@ -93,6 +93,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
ValueKey<String> _mapKey = const ValueKey('initial'); ValueKey<String> _mapKey = const ValueKey('initial');
double _pathDistanceMeters = 0.0; double _pathDistanceMeters = 0.0;
bool _showNodeLabels = true; bool _showNodeLabels = true;
Contact? _targetContact;
String _formatPathPrefixes(Uint8List pathBytes) { String _formatPathPrefixes(Uint8List pathBytes) {
return pathBytes return pathBytes
@@ -158,21 +159,16 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
}); });
} }
final Uint8List path; final pathTmp = widget.reversePathAround
Uint8List pathTmp = widget.reversePathRound
? Uint8List.fromList(widget.path.reversed.toList()) ? Uint8List.fromList(widget.path.reversed.toList())
: widget.path; : widget.path;
if (widget.flipPathRound) { final path = widget.flipPathAround ? buildPath(pathTmp) : pathTmp;
path = buildPath(pathTmp);
} else {
path = pathTmp;
}
appLogger.info( appLogger.info(
'Initiating path trace with path: ${_formatPathPrefixes(path)}', 'Initiating path trace with path: ${_formatPathPrefixes(path)}',
tag: 'PathTraceMapScreen', tag: 'PathTraceMapScreen',
noNotify: !mounted,
); );
final connector = Provider.of<MeshCoreConnector>(context, listen: false); final connector = Provider.of<MeshCoreConnector>(context, listen: false);
@@ -263,10 +259,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
.toList(); .toList();
Map<int, Contact> pathContacts = {}; Map<int, Contact> pathContacts = {};
final contacts = <Contact>[ final contacts = connector.allContacts;
...connector.contacts,
...connector.discoveredContacts,
];
contacts.where((c) => c.type != advTypeChat).forEach((repeater) { contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
for (var repeaterData in pathData) { for (var repeaterData in pathData) {
if (listEquals( if (listEquals(
@@ -312,18 +305,21 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
// Compute endpoint position for the target contact. // Compute endpoint position for the target contact.
LatLng? targetPos; LatLng? targetPos;
bool targetGuessed = false; bool targetGuessed = false;
final target = widget.targetContact; _targetContact = widget.targetContact;
if (target != null) {
if (target.hasLocation) { if (_targetContact != null) {
targetPos = LatLng(target.latitude!, target.longitude!); final tc = _targetContact!;
} else if (pathData.isNotEmpty) { if (tc.hasLocation) {
targetPos = LatLng(tc.latitude!, tc.longitude!);
} else if (widget.path.length > 1) {
// Infer from the last hop: average GPS contacts sharing that hop. // Infer from the last hop: average GPS contacts sharing that hop.
// For a round-trip path (flipPathRound), the target-side hop sits // For a round-trip path (flipPathAround/reversePathAround), the target-side hop
// in the middle of the symmetric sequence; .last is the local side. // sits in the middle of the symmetric sequence; .last is the local side.
final lastHop = (widget.flipPathRound && pathData.length > 1) final lastHop = widget.reversePathAround
? pathData[(pathData.length - 1) ~/ 2] ? widget.path.first
: pathData.last; : widget.path.last;
final peers = connector.contacts
final peers = connector.allContacts
.where( .where(
(c) => (c) =>
c.hasLocation && c.hasLocation &&
@@ -339,12 +335,34 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
peers.map((c) => c.longitude!).reduce((a, b) => a + b) / peers.map((c) => c.longitude!).reduce((a, b) => a + b) /
peers.length; peers.length;
const offsetDeg = 0.003; const offsetDeg = 0.003;
final angle = (target.publicKey[1] / 255.0) * 2 * pi; final angle = (tc.publicKey[1] / 255.0) * 2 * pi;
targetPos = LatLng( targetPos = LatLng(
lat + offsetDeg * cos(angle), lat + offsetDeg * cos(angle),
lon + offsetDeg * sin(angle), lon + offsetDeg * sin(angle),
); );
targetGuessed = true; targetGuessed = true;
} else if (inferredPositions.containsKey(lastHop)) {
final lat = inferredPositions[lastHop]!.latitude;
final lon = inferredPositions[lastHop]!.longitude;
const offsetDeg = 0.003;
final angle = (tc.publicKey[1] / 255.0) * 2 * pi;
targetPos = LatLng(
lat + offsetDeg * cos(angle),
lon + offsetDeg * sin(angle),
);
targetGuessed = true;
} else {
// As a last resort, just place it at the same position as the last hop.
final contact = pathContacts[lastHop];
if (contact != null && contact.hasLocation) {
const offsetDeg = 0.003;
final angle = (tc.publicKey[1] / 255.0) * 2 * pi;
targetPos = LatLng(
contact.latitude! + offsetDeg * cos(angle),
contact.longitude! + offsetDeg * sin(angle),
);
targetGuessed = true;
}
} }
} }
} }
@@ -353,7 +371,12 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
_points = <LatLng>[]; _points = <LatLng>[];
_points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!)); _points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
int hopLast = 0;
int hopLastLast = 0;
for (final hop in _traceData!.pathData) { for (final hop in _traceData!.pathData) {
if (hop == hopLastLast && widget.flipPathAround) {
break; //skip duplicate hops in round-trip paths
}
final contact = _traceData!.pathContacts[hop]; final contact = _traceData!.pathContacts[hop];
if (contact != null && contact.hasLocation) { if (contact != null && contact.hasLocation) {
_points.add(LatLng(contact.latitude!, contact.longitude!)); _points.add(LatLng(contact.latitude!, contact.longitude!));
@@ -361,8 +384,14 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
final inferred = inferredPositions[hop]; final inferred = inferredPositions[hop];
if (inferred != null) _points.add(inferred); if (inferred != null) _points.add(inferred);
} }
hopLastLast = hopLast;
hopLast = hop;
}
if (targetPos != null) {
if (_targetContact != null && _targetContact!.type == advTypeChat) {
_points.add(targetPos);
}
} }
if (targetPos != null) _points.add(targetPos);
_polylines = _points.length > 1 _polylines = _points.length > 1
? [ ? [
Polyline( Polyline(
@@ -451,7 +480,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
], ],
), ),
), ),
if (_hasData) _buildMapPathTrace(context, tileCache), if (_hasData)
_buildMapPathTrace(context, tileCache, _targetContact),
if (_points.isEmpty && if (_points.isEmpty &&
!_hasData && !_hasData &&
!_isLoading && !_isLoading &&
@@ -480,17 +510,28 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
List<Marker> _buildHopMarkers( List<Marker> _buildHopMarkers(
List<int> pathData, { List<int> pathData, {
required bool showLabels, required bool showLabels,
required Contact? target,
}) { }) {
final markers = <Marker>[]; final markers = <Marker>[];
int hopLast = 0;
int hopLastLast = 0;
for (final hop in pathData) { for (final hop in pathData) {
final contact = _traceData!.pathContacts[hop]; final contact = _traceData!.pathContacts[hop];
final inferred = _inferredHopPositions[hop]; final inferred = _inferredHopPositions[hop];
final hasGps = contact != null && contact.hasLocation; final hasGps = contact != null && contact.hasLocation;
if (!hasGps && inferred == null) continue; if (hop == hopLastLast && widget.flipPathAround) {
continue; //skip duplicate hops in round-trip paths
}
if (!hasGps && inferred == null) {
hopLastLast = hopLast;
hopLast = hop;
continue; //skip hops with no GPS and no inferred position
}
final point = hasGps final point = hasGps
? LatLng(contact.latitude!, contact.longitude!) ? LatLng(contact.latitude!, contact.longitude!)
: inferred!; : inferred!;
final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase(); final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase();
markers.add( markers.add(
Marker( Marker(
point: point, point: point,
@@ -532,6 +573,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
), ),
); );
} }
hopLastLast = hopLast;
hopLast = hop;
} }
final selfLat = context.read<MeshCoreConnector>().selfLatitude; final selfLat = context.read<MeshCoreConnector>().selfLatitude;
@@ -581,9 +624,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
// Add target contact endpoint marker. // Add target contact endpoint marker.
final targetPos = _targetContactPosition; final targetPos = _targetContactPosition;
if (targetPos != null) { if (targetPos != null && target != null && target.type == advTypeChat) {
final isGuessed = _targetContactIsGuessed; final isGuessed = _targetContactIsGuessed;
final targetName = widget.targetContact?.name ?? '?'; final targetName = target.name;
markers.add( markers.add(
Marker( Marker(
point: targetPos, point: targetPos,
@@ -719,6 +762,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
Widget _buildMapPathTrace( Widget _buildMapPathTrace(
BuildContext context, BuildContext context,
MapTileCacheService tileCache, MapTileCacheService tileCache,
Contact? target,
) { ) {
return FlutterMap( return FlutterMap(
key: _mapKey, key: _mapKey,
@@ -757,6 +801,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
markers: _buildHopMarkers( markers: _buildHopMarkers(
_traceData!.pathData, _traceData!.pathData,
showLabels: _showNodeLabels, showLabels: _showNodeLabels,
target: target,
), ),
), ),
], ],
+13 -2
View File
@@ -77,11 +77,22 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
}); });
} }
int _resolveRepeaterIndex = -1;
Contact _resolveRepeater(MeshCoreConnector connector) { Contact _resolveRepeater(MeshCoreConnector connector) {
return connector.contacts.firstWhere( if (_resolveRepeaterIndex >= 0 &&
_resolveRepeaterIndex < connector.contacts.length &&
connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
widget.repeater.publicKeyHex) {
return connector.contacts[_resolveRepeaterIndex];
}
_resolveRepeaterIndex = connector.contacts.indexWhere(
(c) => c.publicKeyHex == widget.repeater.publicKeyHex, (c) => c.publicKeyHex == widget.repeater.publicKeyHex,
orElse: () => widget.repeater,
); );
if (_resolveRepeaterIndex == -1) {
return widget.repeater;
}
return connector.contacts[_resolveRepeaterIndex];
} }
void _handleTextMessageResponse(Uint8List frame) { void _handleTextMessageResponse(Uint8List frame) {
+1 -2
View File
@@ -205,8 +205,7 @@ class RepeaterHubScreen extends StatelessWidget {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => builder: (context) => TelemetryScreen(contact: repeater),
TelemetryScreen(repeater: repeater, password: password),
), ),
); );
}, },
+13 -2
View File
@@ -129,11 +129,22 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
_commandService?.handleResponse(widget.repeater, parsed.text); _commandService?.handleResponse(widget.repeater, parsed.text);
} }
int _resolveRepeaterIndex = -1;
Contact _resolveRepeater(MeshCoreConnector connector) { Contact _resolveRepeater(MeshCoreConnector connector) {
return connector.contacts.firstWhere( if (_resolveRepeaterIndex >= 0 &&
_resolveRepeaterIndex < connector.contacts.length &&
connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
widget.repeater.publicKeyHex) {
return connector.contacts[_resolveRepeaterIndex];
}
_resolveRepeaterIndex = connector.contacts.indexWhere(
(c) => c.publicKeyHex == widget.repeater.publicKeyHex, (c) => c.publicKeyHex == widget.repeater.publicKeyHex,
orElse: () => widget.repeater,
); );
if (_resolveRepeaterIndex == -1) {
return widget.repeater;
}
return connector.contacts[_resolveRepeaterIndex];
} }
bool _matchesRepeaterPrefix(Uint8List prefix) { bool _matchesRepeaterPrefix(Uint8List prefix) {
+13 -2
View File
@@ -91,11 +91,22 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
}); });
} }
int _resolveRepeaterIndex = -1;
Contact _resolveRepeater(MeshCoreConnector connector) { Contact _resolveRepeater(MeshCoreConnector connector) {
return connector.contacts.firstWhere( if (_resolveRepeaterIndex >= 0 &&
_resolveRepeaterIndex < connector.contacts.length &&
connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
widget.repeater.publicKeyHex) {
return connector.contacts[_resolveRepeaterIndex];
}
_resolveRepeaterIndex = connector.contacts.indexWhere(
(c) => c.publicKeyHex == widget.repeater.publicKeyHex, (c) => c.publicKeyHex == widget.repeater.publicKeyHex,
orElse: () => widget.repeater,
); );
if (_resolveRepeaterIndex == -1) {
return widget.repeater;
}
return connector.contacts[_resolveRepeaterIndex];
} }
void _handleTextMessageResponse(Uint8List frame) { void _handleTextMessageResponse(Uint8List frame) {
+134 -44
View File
@@ -287,10 +287,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
const Divider(height: 1), const Divider(height: 1),
ListTile( ListTile(
leading: const Icon(Icons.visibility_off_outlined), leading: const Icon(Icons.visibility_off_outlined),
title: Text(l10n.settings_privacyMode), title: Text(l10n.settings_privacy),
subtitle: Text(l10n.settings_privacyModeSubtitle), subtitle: Text(l10n.settings_privacySubtitle),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () => _togglePrivacy(context, connector), onTap: () => _privacySettings(context, connector),
), ),
], ],
), ),
@@ -657,47 +657,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
); );
} }
void _togglePrivacy(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(l10n.settings_privacyMode),
content: Text(l10n.settings_privacyModeToggle),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
TextButton(
onPressed: () async {
Navigator.pop(context);
await connector.setPrivacyMode(true);
await connector.refreshDeviceInfo();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_privacyModeEnabled)),
);
},
child: Text(l10n.common_enable),
),
TextButton(
onPressed: () async {
Navigator.pop(context);
await connector.setPrivacyMode(false);
await connector.refreshDeviceInfo();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_privacyModeDisabled)),
);
},
child: Text(l10n.common_disable),
),
],
),
);
}
void _sendAdvert(BuildContext context, MeshCoreConnector connector) { void _sendAdvert(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n; final l10n = context.l10n;
connector.sendSelfAdvert(flood: true); connector.sendSelfAdvert(flood: true);
@@ -977,6 +936,137 @@ class _SettingsScreenState extends State<SettingsScreen> {
} }
} }
void _privacySettings(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
int telemetryMode = connector.telemetryModeBase;
int telemetryLocMode = connector.telemetryModeLoc;
int telemetryEnvMode = connector.telemetryModeEnv;
bool advertLocPolicy = connector.advertLocationPolicy == 0 ? false : true;
int multiAcks = connector.multiAcks;
final telemModeBase = [
DropdownMenuItem(value: teleModeDeny, child: Text(l10n.settings_denyAll)),
DropdownMenuItem(
value: teleModeAllowFlags,
child: Text(l10n.settings_allowByContact),
),
DropdownMenuItem(
value: teleModeAllowAll,
child: Text(l10n.settings_allowAll),
),
];
showDialog(
context: context,
builder: (dialogContext) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: Text(l10n.settings_privacy),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.settings_privacySettingsDescription),
const SizedBox(height: 16),
FeatureToggleRow(
title: l10n.settings_advertLocation,
subtitle: l10n.settings_advertLocationSubtitle,
value: advertLocPolicy,
onChanged: (value) {
setDialogState(() => advertLocPolicy = value);
advertLocPolicy = value;
},
),
const SizedBox(height: 8),
DropdownButtonFormField<int>(
initialValue: telemetryMode,
decoration: InputDecoration(
labelText: l10n.settings_telemetryBaseMode,
border: const OutlineInputBorder(),
),
items: telemModeBase,
onChanged: (value) {
if (value != null) {
setDialogState(() => telemetryMode = value);
}
},
),
const SizedBox(height: 16),
DropdownButtonFormField<int>(
initialValue: telemetryLocMode,
decoration: InputDecoration(
labelText: l10n.settings_telemetryLocationMode,
border: const OutlineInputBorder(),
),
items: telemModeBase,
onChanged: (value) {
if (value != null) {
setDialogState(() => telemetryLocMode = value);
}
},
),
const SizedBox(height: 16),
DropdownButtonFormField<int>(
initialValue: telemetryEnvMode,
decoration: InputDecoration(
labelText: l10n.settings_telemetryEnvironmentMode,
border: const OutlineInputBorder(),
),
items: telemModeBase,
onChanged: (value) {
if (value != null) {
setDialogState(() => telemetryEnvMode = value);
}
},
),
const SizedBox(height: 16),
Text(
l10n.settings_multiAck(multiAcks.toString()),
style: Theme.of(context).textTheme.bodyMedium,
),
Slider(
value: multiAcks.toDouble(),
min: 0,
max: 2,
divisions: 2,
label: multiAcks.toString(),
onChanged: (value) {
setDialogState(() => multiAcks = value.round());
},
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
TextButton(
onPressed: () async {
Navigator.pop(context);
await connector.setTelemetryModeBase(
telemetryMode,
telemetryLocMode,
telemetryEnvMode,
advertLocPolicy ? 1 : 0,
multiAcks,
);
await connector.refreshDeviceInfo();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_telemetryModeUpdated)),
);
},
child: Text(l10n.common_save),
),
],
),
),
);
}
class _RadioSettingsDialog extends StatefulWidget { class _RadioSettingsDialog extends StatefulWidget {
final MeshCoreConnector connector; final MeshCoreConnector connector;
+15 -2
View File
@@ -5,6 +5,7 @@ import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart'; import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../services/app_settings_service.dart';
import '../utils/platform_info.dart'; import '../utils/platform_info.dart';
import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/adaptive_app_bar_title.dart';
import 'contacts_screen.dart'; import 'contacts_screen.dart';
@@ -27,8 +28,14 @@ class _TcpScreenState extends State<TcpScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_hostController = TextEditingController(); _hostController = TextEditingController(
_portController = TextEditingController(text: '5000'); text: context.read<AppSettingsService>().settings.tcpServerAddress,
);
_portController = TextEditingController(
text: context.read<AppSettingsService>().settings.tcpServerPort > 0
? context.read<AppSettingsService>().settings.tcpServerPort.toString()
: '',
);
_connector = context.read<MeshCoreConnector>(); _connector = context.read<MeshCoreConnector>();
_connectionListener = () { _connectionListener = () {
@@ -39,6 +46,12 @@ class _TcpScreenState extends State<TcpScreen> {
if (_connector.state == MeshCoreConnectionState.connected && if (_connector.state == MeshCoreConnectionState.connected &&
_connector.isTcpTransportConnected && _connector.isTcpTransportConnected &&
!_navigatedToContacts) { !_navigatedToContacts) {
context.read<AppSettingsService>().setTcpServerAddress(
_hostController.text,
);
context.read<AppSettingsService>().setTcpServerPort(
int.tryParse(_portController.text) ?? 0,
);
_navigatedToContacts = true; _navigatedToContacts = true;
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const ContactsScreen()), MaterialPageRoute(builder: (_) => const ContactsScreen()),
+95 -73
View File
@@ -10,30 +10,22 @@ import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../services/app_settings_service.dart'; import '../services/app_settings_service.dart';
import '../services/repeater_command_service.dart'; import '../services/repeater_command_service.dart';
import '../utils/app_logger.dart';
import '../widgets/path_management_dialog.dart'; import '../widgets/path_management_dialog.dart';
import '../helpers/cayenne_lpp.dart'; import '../helpers/cayenne_lpp.dart';
import '../utils/battery_utils.dart'; import '../utils/battery_utils.dart';
class TelemetryScreen extends StatefulWidget { class TelemetryScreen extends StatefulWidget {
final Contact repeater; final Contact contact;
final String password;
const TelemetryScreen({ const TelemetryScreen({super.key, required this.contact});
super.key,
required this.repeater,
required this.password,
});
@override @override
State<TelemetryScreen> createState() => _TelemetryScreenState(); State<TelemetryScreen> createState() => _TelemetryScreenState();
} }
class _TelemetryScreenState extends State<TelemetryScreen> { class _TelemetryScreenState extends State<TelemetryScreen> {
static const int _statusPayloadOffset = 8; int _tagData = 0;
static const int _statusStatsSize = 52;
static const int _statusResponseBytes =
_statusPayloadOffset + _statusStatsSize;
Uint8List _tagData = Uint8List(4);
bool _isLoading = false; bool _isLoading = false;
bool _isLoaded = false; bool _isLoaded = false;
@@ -44,6 +36,26 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
PathSelection? _pendingStatusSelection; PathSelection? _pendingStatusSelection;
List<Map<String, dynamic>>? _parsedTelemetry; List<Map<String, dynamic>>? _parsedTelemetry;
int _tripTime = 0;
int _resolveContactIndex = -1;
Contact _resolveContact(MeshCoreConnector connector) {
if (_resolveContactIndex >= 0 &&
_resolveContactIndex < connector.contacts.length &&
connector.contacts[_resolveContactIndex].publicKeyHex ==
widget.contact.publicKeyHex) {
return connector.contacts[_resolveContactIndex];
}
_resolveContactIndex = connector.contacts.indexWhere(
(c) => c.publicKeyHex == widget.contact.publicKeyHex,
);
if (_resolveContactIndex == -1) {
return widget.contact;
}
return connector.contacts[_resolveContactIndex];
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -60,27 +72,62 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
// Listen for incoming text messages from the repeater // Listen for incoming text messages from the repeater
_frameSubscription = connector.receivedFrames.listen((frame) { _frameSubscription = connector.receivedFrames.listen((frame) {
if (frame.isEmpty) return; if (frame.isEmpty) return;
final reader = BufferReader(frame);
try {
final cmd = reader.readByte();
if (cmd == respCodeSent) {
reader.skipBytes(1); // Skip the reserved byte
_tagData = reader.readUInt32LE();
_tripTime = reader.readUInt32LE();
_statusTimeout?.cancel();
_statusTimeout = Timer(Duration(milliseconds: _tripTime), () {
if (!mounted) return;
setState(() {
_isLoading = false;
_isLoaded = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.telemetry_requestTimeout),
backgroundColor: Colors.red,
),
);
_recordTelemetryResult(false);
});
}
if (frame[0] == respCodeSent) { // Check if it's a binary response
_tagData = frame.sublist(2, 6); if (cmd == pushCodeBinaryResponse) {
} if (!mounted) return;
reader.skipBytes(1); // Skip the reserved byte
if (reader.readUInt32LE() != _tagData) return;
_handleTelemetryResponse(reader.readRemainingBytes());
}
// Check if it's a binary response // Check if it's a telemetry response (for chat contacts)
if (frame[0] == pushCodeBinaryResponse && if (cmd == pushCodeTelemetryResponse) {
listEquals(frame.sublist(2, 6), _tagData)) { reader.skipBytes(1); // Skip the reserved byte
if (!mounted) return; final pubkey = reader.readBytes(6);
_handleStatusResponse(frame.sublist(6)); if (!mounted) return;
if (!listEquals(widget.contact.publicKey.sublist(0, 6), pubkey)) {
return;
}
_handleTelemetryResponse(reader.readRemainingBytes());
}
} catch (e) {
appLogger.error('Error parsing incoming frame: $e');
// If parsing fails, ignore the frame
} }
}); });
} }
void _handleStatusResponse(Uint8List frame) { void _handleTelemetryResponse(Uint8List frame) {
final parsedTelemetry = CayenneLpp.parseByChannel(frame); final parsedTelemetry = CayenneLpp.parseByChannel(frame);
final batteryMv = _extractTelemetryBatteryMillivolts(parsedTelemetry); final batteryMv = _extractTelemetryBatteryMillivolts(parsedTelemetry);
if (batteryMv != null) { if (batteryMv != null) {
final connector = Provider.of<MeshCoreConnector>(context, listen: false); final connector = Provider.of<MeshCoreConnector>(context, listen: false);
connector.updateRepeaterBatterySnapshot( connector.updateRepeaterBatterySnapshot(
widget.repeater.publicKeyHex, widget.contact.publicKeyHex,
batteryMv, batteryMv,
source: 'telemetry', source: 'telemetry',
); );
@@ -105,13 +152,6 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
}); });
} }
Contact _resolveRepeater(MeshCoreConnector connector) {
return connector.contacts.firstWhere(
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
orElse: () => widget.repeater,
);
}
Future<void> _loadTelemetry() async { Future<void> _loadTelemetry() async {
if (_commandService == null) return; if (_commandService == null) return;
@@ -121,41 +161,20 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
}); });
try { try {
final connector = Provider.of<MeshCoreConnector>(context, listen: false); final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final repeater = _resolveRepeater(connector); final selection = await connector.preparePathForContactSend(
final selection = await connector.preparePathForContactSend(repeater); _resolveContact(connector),
);
_pendingStatusSelection = selection; _pendingStatusSelection = selection;
final frame = buildSendBinaryReq( Uint8List frame;
repeater.publicKey, if (widget.contact.type != advTypeChat) {
payload: Uint8List.fromList([reqTypeGetTelemetry]), frame = buildSendBinaryReq(
); widget.contact.publicKey,
await connector.sendFrame(frame); payload: Uint8List.fromList([reqTypeGetTelemetry]),
final pathLengthValue = selection.useFlood ? -1 : selection.hopCount;
var messageBytes = frame.length >= _statusResponseBytes
? frame.length
: _statusResponseBytes;
if (messageBytes < maxFrameSize) {
messageBytes = maxFrameSize;
}
final timeoutMs = connector.calculateTimeout(
pathLength: pathLengthValue,
messageBytes: messageBytes,
);
_statusTimeout?.cancel();
_statusTimeout = Timer(Duration(milliseconds: timeoutMs), () {
if (!mounted) return;
setState(() {
_isLoading = false;
_isLoaded = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.telemetry_requestTimeout),
backgroundColor: Colors.red,
),
); );
_recordStatusResult(false); } else {
}); frame = buildSendTelemetryReq(widget.contact.publicKey);
}
await connector.sendFrame(frame);
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
setState(() { setState(() {
@@ -173,12 +192,16 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
} }
} }
void _recordStatusResult(bool success) { void _recordTelemetryResult(bool success) {
final selection = _pendingStatusSelection; final selection = _pendingStatusSelection;
if (selection == null) return; if (selection == null) return;
final connector = Provider.of<MeshCoreConnector>(context, listen: false); final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final repeater = _resolveRepeater(connector); connector.recordRepeaterPathResult(
connector.recordRepeaterPathResult(repeater, selection, success, null); widget.contact,
selection,
success,
null,
);
_pendingStatusSelection = null; _pendingStatusSelection = null;
} }
@@ -196,8 +219,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
final connector = context.watch<MeshCoreConnector>(); final connector = context.watch<MeshCoreConnector>();
final settings = context.watch<AppSettingsService>().settings; final settings = context.watch<AppSettingsService>().settings;
final isImperialUnits = settings.unitSystem == UnitSystem.imperial; final isImperialUnits = settings.unitSystem == UnitSystem.imperial;
final repeater = _resolveRepeater(connector); final isFloodMode = widget.contact.pathOverride == -1;
final isFloodMode = repeater.pathOverride == -1;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
@@ -210,7 +232,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
), ),
Text( Text(
repeater.name, widget.contact.name,
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
@@ -225,9 +247,9 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
tooltip: l10n.repeater_routingMode, tooltip: l10n.repeater_routingMode,
onSelected: (mode) async { onSelected: (mode) async {
if (mode == 'flood') { if (mode == 'flood') {
await connector.setPathOverride(repeater, pathLen: -1); await connector.setPathOverride(widget.contact, pathLen: -1);
} else { } else {
await connector.setPathOverride(repeater, pathLen: null); await connector.setPathOverride(widget.contact, pathLen: null);
} }
}, },
itemBuilder: (context) => [ itemBuilder: (context) => [
@@ -283,7 +305,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
icon: const Icon(Icons.timeline), icon: const Icon(Icons.timeline),
tooltip: l10n.repeater_pathManagement, tooltip: l10n.repeater_pathManagement,
onPressed: () => onPressed: () =>
PathManagementDialog.show(context, contact: repeater), PathManagementDialog.show(context, contact: widget.contact),
), ),
IconButton( IconButton(
icon: _isLoading icon: _isLoading
@@ -437,7 +459,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
final l10n = context.l10n; final l10n = context.l10n;
final connector = context.watch<MeshCoreConnector>(); final connector = context.watch<MeshCoreConnector>();
final batteryMv = final batteryMv =
connector.getRepeaterBatteryMillivolts(widget.repeater.publicKeyHex) ?? connector.getRepeaterBatteryMillivolts(widget.contact.publicKeyHex) ??
(telemetryVolts == null ? null : (telemetryVolts * 1000).round()); (telemetryVolts == null ? null : (telemetryVolts * 1000).round());
if (batteryMv == null) return l10n.common_notAvailable; if (batteryMv == null) return l10n.common_notAvailable;
final chemistry = _batteryChemistry(); final chemistry = _batteryChemistry();
@@ -449,7 +471,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
String _batteryChemistry() { String _batteryChemistry() {
final settingsService = context.read<AppSettingsService>(); final settingsService = context.read<AppSettingsService>();
return settingsService.batteryChemistryForRepeater( return settingsService.batteryChemistryForRepeater(
widget.repeater.publicKeyHex, widget.contact.publicKeyHex,
); );
} }
+10 -7
View File
@@ -51,6 +51,7 @@ class AppDebugLogService extends ChangeNotifier {
String message, { String message, {
String tag = 'App', String tag = 'App',
AppDebugLogLevel level = AppDebugLogLevel.info, AppDebugLogLevel level = AppDebugLogLevel.info,
bool noNotify = false,
}) { }) {
if (!_enabled && !kDebugMode) return; if (!_enabled && !kDebugMode) return;
if (!_enabled) { if (!_enabled) {
@@ -72,22 +73,24 @@ class AppDebugLogService extends ChangeNotifier {
_entries.removeRange(0, _entries.length - maxEntries); _entries.removeRange(0, _entries.length - maxEntries);
} }
notifyListeners(); if (!noNotify) {
notifyListeners();
}
// Also print to console for development // Also print to console for development
debugPrint('[$tag] $message'); debugPrint('[$tag] $message');
} }
void info(String message, {String tag = 'App'}) { void info(String message, {String tag = 'App', bool noNotify = false}) {
log(message, tag: tag, level: AppDebugLogLevel.info); log(message, tag: tag, level: AppDebugLogLevel.info, noNotify: noNotify);
} }
void warn(String message, {String tag = 'App'}) { void warn(String message, {String tag = 'App', bool noNotify = false}) {
log(message, tag: tag, level: AppDebugLogLevel.warning); log(message, tag: tag, level: AppDebugLogLevel.warning, noNotify: noNotify);
} }
void error(String message, {String tag = 'App'}) { void error(String message, {String tag = 'App', bool noNotify = false}) {
log(message, tag: tag, level: AppDebugLogLevel.error); log(message, tag: tag, level: AppDebugLogLevel.error, noNotify: noNotify);
} }
void clear() { void clear() {
+8
View File
@@ -182,4 +182,12 @@ class AppSettingsService extends ChangeNotifier {
..remove(channelName); ..remove(channelName);
await updateSettings(_settings.copyWith(mutedChannels: updated)); await updateSettings(_settings.copyWith(mutedChannels: updated));
} }
Future<void> setTcpServerAddress(String value) async {
await updateSettings(_settings.copyWith(tcpServerAddress: value));
}
Future<void> setTcpServerPort(int value) async {
await updateSettings(_settings.copyWith(tcpServerPort: value));
}
} }
+1 -1
View File
@@ -65,7 +65,7 @@ class ChatTextScaleService extends ChangeNotifier {
void _commitScale() { void _commitScale() {
_saveTimer?.cancel(); _saveTimer?.cancel();
PrefsManager.instance.setDouble(_prefKey, _scale); unawaited(PrefsManager.instance.setDouble(_prefKey, _scale));
} }
double _clamp(double value) => value.clamp(_minScale, _maxScale).toDouble(); double _clamp(double value) => value.clamp(_minScale, _maxScale).toDouble();
+154
View File
@@ -0,0 +1,154 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import '../storage/prefs_manager.dart';
import '../utils/contact_search.dart';
const String contactsAllGroupsValue = '__all__';
enum ChannelSortOption { manual, name, latestMessages, unread }
class UiViewStateService extends ChangeNotifier {
static const _keyContactsSelectedGroupName = 'ui_contacts_selected_group';
static const _keyContactsSortOption = 'ui_contacts_sort_option';
static const _keyContactsShowUnreadOnly = 'ui_contacts_show_unread_only';
static const _keyContactsTypeFilter = 'ui_contacts_type_filter';
static const _keyChannelsSortOption = 'ui_channels_sort_option';
static const _keyChannelsSortIndexLegacy = 'ui_channels_sort_index';
String _contactsSelectedGroupName = contactsAllGroupsValue;
String _contactsSearchText = '';
bool _contactsSearchExpanded = false;
ContactSortOption _contactsSortOption = ContactSortOption.lastSeen;
bool _contactsShowUnreadOnly = false;
ContactTypeFilter _contactsTypeFilter = ContactTypeFilter.all;
String _channelsSearchText = '';
ChannelSortOption _channelsSortOption = ChannelSortOption.manual;
String get contactsSelectedGroupName => _contactsSelectedGroupName;
String get contactsSearchText => _contactsSearchText;
bool get contactsSearchExpanded => _contactsSearchExpanded;
ContactSortOption get contactsSortOption => _contactsSortOption;
bool get contactsShowUnreadOnly => _contactsShowUnreadOnly;
ContactTypeFilter get contactsTypeFilter => _contactsTypeFilter;
String get channelsSearchText => _channelsSearchText;
ChannelSortOption get channelsSortOption => _channelsSortOption;
Future<void> initialize() async {
final prefs = PrefsManager.instance;
final selectedGroupName = prefs.getString(_keyContactsSelectedGroupName);
if (selectedGroupName != null && selectedGroupName.isNotEmpty) {
_contactsSelectedGroupName = selectedGroupName;
}
final sortStr = prefs.getString(_keyContactsSortOption);
if (sortStr != null) {
_contactsSortOption = ContactSortOption.values.firstWhere(
(e) => e.name == sortStr,
orElse: () => ContactSortOption.lastSeen,
);
}
_contactsShowUnreadOnly =
prefs.getBool(_keyContactsShowUnreadOnly) ?? false;
final typeStr = prefs.getString(_keyContactsTypeFilter);
if (typeStr != null) {
_contactsTypeFilter = ContactTypeFilter.values.firstWhere(
(e) => e.name == typeStr,
orElse: () => ContactTypeFilter.all,
);
}
final channelSortStr = prefs.getString(_keyChannelsSortOption);
if (channelSortStr != null) {
_channelsSortOption = ChannelSortOption.values.firstWhere(
(e) => e.name == channelSortStr,
orElse: () => ChannelSortOption.manual,
);
return;
}
// Backward compatibility for old persisted index format.
switch (prefs.getInt(_keyChannelsSortIndexLegacy) ?? 0) {
case 0:
_channelsSortOption = ChannelSortOption.manual;
break;
case 1:
_channelsSortOption = ChannelSortOption.name;
break;
case 2:
_channelsSortOption = ChannelSortOption.latestMessages;
break;
case 3:
_channelsSortOption = ChannelSortOption.unread;
break;
default:
_channelsSortOption = ChannelSortOption.manual;
}
}
void setContactsSelectedGroupName(String value) {
if (_contactsSelectedGroupName == value) return;
_contactsSelectedGroupName = value;
notifyListeners();
unawaited(
PrefsManager.instance.setString(_keyContactsSelectedGroupName, value),
);
}
void setContactsSearchText(String value) {
if (_contactsSearchText == value) return;
_contactsSearchText = value;
notifyListeners();
}
void setContactsSearchExpanded(bool value) {
if (_contactsSearchExpanded == value) return;
_contactsSearchExpanded = value;
notifyListeners();
}
void setContactsSortOption(ContactSortOption value) {
if (_contactsSortOption == value) return;
_contactsSortOption = value;
notifyListeners();
unawaited(
PrefsManager.instance.setString(_keyContactsSortOption, value.name),
);
}
void setContactsShowUnreadOnly(bool value) {
if (_contactsShowUnreadOnly == value) return;
_contactsShowUnreadOnly = value;
notifyListeners();
unawaited(PrefsManager.instance.setBool(_keyContactsShowUnreadOnly, value));
}
void setContactsTypeFilter(ContactTypeFilter value) {
if (_contactsTypeFilter == value) return;
_contactsTypeFilter = value;
notifyListeners();
unawaited(
PrefsManager.instance.setString(_keyContactsTypeFilter, value.name),
);
}
void setChannelsSearchText(String value) {
if (_channelsSearchText == value) return;
_channelsSearchText = value;
notifyListeners();
}
void setChannelsSortOption(ChannelSortOption value) {
if (_channelsSortOption == value) return;
_channelsSortOption = value;
notifyListeners();
unawaited(
PrefsManager.instance.setString(_keyChannelsSortOption, value.name),
);
}
}
@@ -189,6 +189,10 @@ class UsbSerialService {
serial.setStopBits1(); serial.setStopBits1();
serial.setFlowControlNone(); serial.setFlowControlNone();
serial.setRTS(false); serial.setRTS(false);
// Toggle DTR lowhigh so the device sees a fresh connection even
// if the previous disconnect didn't cleanly signal DTR drop.
serial.setDTR(false);
await Future<void>.delayed(const Duration(milliseconds: 50));
serial.setDTR(true); serial.setDTR(true);
_serial = serial; _serial = serial;
// Update the normalized port name to whichever candidate succeeded. // Update the normalized port name to whichever candidate succeeded.
@@ -249,6 +253,21 @@ class UsbSerialService {
_status = UsbSerialStatus.connected; _status = UsbSerialStatus.connected;
} }
Future<void> writeRaw(Uint8List data) async {
if (!isConnected) {
throw StateError('USB serial port is not open');
}
if (_useAndroidUsbHost) {
try {
await _androidMethodChannel.invokeMethod<void>('write', {'data': data});
} on PlatformException catch (error) {
throw StateError(error.message ?? error.code);
}
} else {
_serial!.write(data);
}
}
Future<void> write(Uint8List data) async { Future<void> write(Uint8List data) async {
if (!isConnected) { if (!isConnected) {
throw StateError('USB serial port is not open'); throw StateError('USB serial port is not open');
@@ -300,6 +319,7 @@ class UsbSerialService {
_serial = null; _serial = null;
try { try {
if (serial?.isOpen() == FlOpenStatus.open) { if (serial?.isOpen() == FlOpenStatus.open) {
serial?.setDTR(false);
serial?.closePort(); serial?.closePort();
} }
} catch (_) { } catch (_) {
@@ -350,6 +370,7 @@ class UsbSerialService {
final serial = _serial; final serial = _serial;
try { try {
if (serial?.isOpen() == FlOpenStatus.open) { if (serial?.isOpen() == FlOpenStatus.open) {
serial?.setDTR(false);
serial?.closePort(); // synchronous C call kills the SerialThread serial?.closePort(); // synchronous C call kills the SerialThread
} }
} catch (_) {} } catch (_) {}
+11
View File
@@ -127,6 +127,17 @@ class UsbSerialService {
} }
} }
Future<void> writeRaw(Uint8List data) async {
if (!isConnected || _writer == null) {
throw StateError('USB serial port is not open');
}
final promise = _writer!.callMethod<JSPromise<JSAny?>>(
'write'.toJS,
data.toJS,
);
await promise.toDart;
}
Future<void> write(Uint8List data) async { Future<void> write(Uint8List data) async {
if (!isConnected || _writer == null) { if (!isConnected || _writer == null) {
throw StateError('USB serial port is not open'); throw StateError('USB serial port is not open');
+8 -7
View File
@@ -23,23 +23,23 @@ class AppLogger {
bool get isEnabled => _enabled; bool get isEnabled => _enabled;
/// Log an info message /// Log an info message
void info(String message, {String tag = 'App'}) { void info(String message, {String tag = 'App', bool noNotify = false}) {
if (_enabled && _service != null) { if (_enabled && _service != null) {
_service!.info(message, tag: tag); _service!.info(message, tag: tag, noNotify: noNotify);
} }
} }
/// Log a warning message /// Log a warning message
void warn(String message, {String tag = 'App'}) { void warn(String message, {String tag = 'App', bool noNotify = false}) {
if (_enabled && _service != null) { if (_enabled && _service != null) {
_service!.warn(message, tag: tag); _service!.warn(message, tag: tag, noNotify: noNotify);
} }
} }
/// Log an error message /// Log an error message
void error(String message, {String tag = 'App'}) { void error(String message, {String tag = 'App', bool noNotify = false}) {
if (_enabled && _service != null) { if (_enabled && _service != null) {
_service!.error(message, tag: tag); _service!.error(message, tag: tag, noNotify: noNotify);
} }
} }
@@ -48,9 +48,10 @@ class AppLogger {
String message, { String message, {
String tag = 'App', String tag = 'App',
AppDebugLogLevel level = AppDebugLogLevel.info, AppDebugLogLevel level = AppDebugLogLevel.info,
bool noNotify = false,
}) { }) {
if (_enabled && _service != null) { if (_enabled && _service != null) {
_service!.log(message, tag: tag, level: level); _service!.log(message, tag: tag, level: level, noNotify: noNotify);
} }
} }
} }
+3
View File
@@ -0,0 +1,3 @@
enum ContactSortOption { lastSeen, recentMessages, name }
enum ContactTypeFilter { all, favorites, users, repeaters, rooms }
+2
View File
@@ -1,5 +1,7 @@
import '../models/contact.dart'; import '../models/contact.dart';
export 'contact_filter_types.dart';
bool matchesContactQuery(Contact contact, String query) { bool matchesContactQuery(Contact contact, String query) {
final normalizedQuery = query.trim().toLowerCase(); final normalizedQuery = query.trim().toLowerCase();
if (normalizedQuery.isEmpty) return true; if (normalizedQuery.isEmpty) return true;
+70 -99
View File
@@ -1,12 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../utils/contact_search.dart';
enum ContactSortOption { lastSeen, recentMessages, name } class SortFilterMenuOption<T> {
final T value;
enum ContactTypeFilter { all, favorites, users, repeaters, rooms }
class SortFilterMenuOption {
final int value;
final String label; final String label;
final bool? checked; final bool? checked;
@@ -17,16 +14,16 @@ class SortFilterMenuOption {
}); });
} }
class SortFilterMenuSection { class SortFilterMenuSection<T> {
final String title; final String title;
final List<SortFilterMenuOption> options; final List<SortFilterMenuOption<T>> options;
const SortFilterMenuSection({required this.title, required this.options}); const SortFilterMenuSection({required this.title, required this.options});
} }
class SortFilterMenu extends StatelessWidget { class SortFilterMenu<T> extends StatelessWidget {
final List<SortFilterMenuSection> sections; final List<SortFilterMenuSection<T>> sections;
final ValueChanged<int> onSelected; final ValueChanged<T> onSelected;
final String tooltip; final String tooltip;
final Widget icon; final Widget icon;
@@ -40,7 +37,7 @@ class SortFilterMenu extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PopupMenuButton<int>( return PopupMenuButton<T>(
icon: icon, icon: icon,
tooltip: tooltip, tooltip: tooltip,
onSelected: onSelected, onSelected: onSelected,
@@ -53,11 +50,11 @@ class SortFilterMenu extends StatelessWidget {
final visibleSections = sections final visibleSections = sections
.where((section) => section.options.isNotEmpty) .where((section) => section.options.isNotEmpty)
.toList(); .toList();
final entries = <PopupMenuEntry<int>>[]; final entries = <PopupMenuEntry<T>>[];
for (int i = 0; i < visibleSections.length; i++) { for (int i = 0; i < visibleSections.length; i++) {
final section = visibleSections[i]; final section = visibleSections[i];
entries.add( entries.add(
PopupMenuItem<int>( PopupMenuItem<T>(
enabled: false, enabled: false,
child: Text(section.title, style: labelStyle), child: Text(section.title, style: labelStyle),
), ),
@@ -65,14 +62,14 @@ class SortFilterMenu extends StatelessWidget {
for (final option in section.options) { for (final option in section.options) {
if (option.checked == null) { if (option.checked == null) {
entries.add( entries.add(
PopupMenuItem<int>( PopupMenuItem<T>(
value: option.value, value: option.value,
child: Text(option.label), child: Text(option.label),
), ),
); );
} else { } else {
entries.add( entries.add(
CheckedPopupMenuItem<int>( CheckedPopupMenuItem<T>(
value: option.value, value: option.value,
checked: option.checked ?? false, checked: option.checked ?? false,
child: Text(option.label), child: Text(option.label),
@@ -90,16 +87,23 @@ class SortFilterMenu extends StatelessWidget {
} }
} }
const int _actionSortRecentMessages = 1; sealed class _ContactsFilterAction {
const int _actionSortName = 2; const _ContactsFilterAction();
const int _actionSortLastSeen = 3; }
const int _actionFilterAll = 4;
const int _actionFilterFavorites = 5; class _SortAction extends _ContactsFilterAction {
const int _actionFilterUsers = 6; final ContactSortOption option;
const int _actionFilterRepeaters = 7; const _SortAction(this.option);
const int _actionFilterRooms = 8; }
const int _actionToggleUnreadOnly = 9;
const int _actionNewGroup = 10; class _TypeFilterAction extends _ContactsFilterAction {
final ContactTypeFilter filter;
const _TypeFilterAction(this.filter);
}
class _ToggleUnreadAction extends _ContactsFilterAction {
const _ToggleUnreadAction();
}
class ContactsFilterMenu extends StatelessWidget { class ContactsFilterMenu extends StatelessWidget {
final ContactSortOption sortOption; final ContactSortOption sortOption;
@@ -108,7 +112,6 @@ class ContactsFilterMenu extends StatelessWidget {
final ValueChanged<ContactSortOption> onSortChanged; final ValueChanged<ContactSortOption> onSortChanged;
final ValueChanged<ContactTypeFilter> onTypeFilterChanged; final ValueChanged<ContactTypeFilter> onTypeFilterChanged;
final ValueChanged<bool> onUnreadOnlyChanged; final ValueChanged<bool> onUnreadOnlyChanged;
final VoidCallback onNewGroup;
const ContactsFilterMenu({ const ContactsFilterMenu({
super.key, super.key,
@@ -118,30 +121,29 @@ class ContactsFilterMenu extends StatelessWidget {
required this.onSortChanged, required this.onSortChanged,
required this.onTypeFilterChanged, required this.onTypeFilterChanged,
required this.onUnreadOnlyChanged, required this.onUnreadOnlyChanged,
required this.onNewGroup,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;
return SortFilterMenu( return SortFilterMenu<_ContactsFilterAction>(
tooltip: l10n.listFilter_tooltip, tooltip: l10n.listFilter_tooltip,
sections: [ sections: [
SortFilterMenuSection( SortFilterMenuSection(
title: l10n.listFilter_sortBy, title: l10n.listFilter_sortBy,
options: [ options: [
SortFilterMenuOption( SortFilterMenuOption(
value: _actionSortRecentMessages, value: _SortAction(ContactSortOption.recentMessages),
label: l10n.listFilter_latestMessages, label: l10n.listFilter_latestMessages,
checked: sortOption == ContactSortOption.recentMessages, checked: sortOption == ContactSortOption.recentMessages,
), ),
SortFilterMenuOption( SortFilterMenuOption(
value: _actionSortLastSeen, value: _SortAction(ContactSortOption.lastSeen),
label: l10n.listFilter_heardRecently, label: l10n.listFilter_heardRecently,
checked: sortOption == ContactSortOption.lastSeen, checked: sortOption == ContactSortOption.lastSeen,
), ),
SortFilterMenuOption( SortFilterMenuOption(
value: _actionSortName, value: _SortAction(ContactSortOption.name),
label: l10n.listFilter_az, label: l10n.listFilter_az,
checked: sortOption == ContactSortOption.name, checked: sortOption == ContactSortOption.name,
), ),
@@ -151,80 +153,66 @@ class ContactsFilterMenu extends StatelessWidget {
title: l10n.listFilter_filters, title: l10n.listFilter_filters,
options: [ options: [
SortFilterMenuOption( SortFilterMenuOption(
value: _actionFilterAll, value: _TypeFilterAction(ContactTypeFilter.all),
label: l10n.listFilter_all, label: l10n.listFilter_all,
checked: typeFilter == ContactTypeFilter.all, checked: typeFilter == ContactTypeFilter.all,
), ),
SortFilterMenuOption( SortFilterMenuOption(
value: _actionFilterFavorites, value: _TypeFilterAction(ContactTypeFilter.favorites),
label: l10n.listFilter_favorites, label: l10n.listFilter_favorites,
checked: typeFilter == ContactTypeFilter.favorites, checked: typeFilter == ContactTypeFilter.favorites,
), ),
SortFilterMenuOption( SortFilterMenuOption(
value: _actionFilterUsers, value: _TypeFilterAction(ContactTypeFilter.users),
label: l10n.listFilter_users, label: l10n.listFilter_users,
checked: typeFilter == ContactTypeFilter.users, checked: typeFilter == ContactTypeFilter.users,
), ),
SortFilterMenuOption( SortFilterMenuOption(
value: _actionFilterRepeaters, value: _TypeFilterAction(ContactTypeFilter.repeaters),
label: l10n.listFilter_repeaters, label: l10n.listFilter_repeaters,
checked: typeFilter == ContactTypeFilter.repeaters, checked: typeFilter == ContactTypeFilter.repeaters,
), ),
SortFilterMenuOption( SortFilterMenuOption(
value: _actionFilterRooms, value: _TypeFilterAction(ContactTypeFilter.rooms),
label: l10n.listFilter_roomServers, label: l10n.listFilter_roomServers,
checked: typeFilter == ContactTypeFilter.rooms, checked: typeFilter == ContactTypeFilter.rooms,
), ),
SortFilterMenuOption( SortFilterMenuOption(
value: _actionToggleUnreadOnly, value: const _ToggleUnreadAction(),
label: l10n.listFilter_unreadOnly, label: l10n.listFilter_unreadOnly,
checked: showUnreadOnly, checked: showUnreadOnly,
), ),
SortFilterMenuOption(
value: _actionNewGroup,
label: l10n.listFilter_newGroup,
),
], ],
), ),
], ],
onSelected: (action) { onSelected: (action) {
switch (action) { switch (action) {
case _actionSortRecentMessages: case _SortAction(:final option):
onSortChanged(ContactSortOption.recentMessages); onSortChanged(option);
break; case _TypeFilterAction(:final filter):
case _actionSortName: onTypeFilterChanged(filter);
onSortChanged(ContactSortOption.name); case _ToggleUnreadAction():
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;
case _actionToggleUnreadOnly:
onUnreadOnlyChanged(!showUnreadOnly); onUnreadOnlyChanged(!showUnreadOnly);
break;
case _actionNewGroup:
onNewGroup();
break;
} }
}, },
); );
} }
} }
sealed class _DiscoveryFilterAction {
const _DiscoveryFilterAction();
}
class _DiscoverySortAction extends _DiscoveryFilterAction {
final ContactSortOption option;
const _DiscoverySortAction(this.option);
}
class _DiscoveryTypeFilterAction extends _DiscoveryFilterAction {
final ContactTypeFilter filter;
const _DiscoveryTypeFilterAction(this.filter);
}
class DiscoveryContactsFilterMenu extends StatelessWidget { class DiscoveryContactsFilterMenu extends StatelessWidget {
final ContactSortOption sortOption; final ContactSortOption sortOption;
final ContactTypeFilter typeFilter; final ContactTypeFilter typeFilter;
@@ -242,19 +230,19 @@ class DiscoveryContactsFilterMenu extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;
return SortFilterMenu( return SortFilterMenu<_DiscoveryFilterAction>(
tooltip: l10n.listFilter_tooltip, tooltip: l10n.listFilter_tooltip,
sections: [ sections: [
SortFilterMenuSection( SortFilterMenuSection(
title: l10n.listFilter_sortBy, title: l10n.listFilter_sortBy,
options: [ options: [
SortFilterMenuOption( SortFilterMenuOption(
value: _actionSortLastSeen, value: _DiscoverySortAction(ContactSortOption.lastSeen),
label: l10n.listFilter_heardRecently, label: l10n.listFilter_heardRecently,
checked: sortOption == ContactSortOption.lastSeen, checked: sortOption == ContactSortOption.lastSeen,
), ),
SortFilterMenuOption( SortFilterMenuOption(
value: _actionSortName, value: _DiscoverySortAction(ContactSortOption.name),
label: l10n.listFilter_az, label: l10n.listFilter_az,
checked: sortOption == ContactSortOption.name, checked: sortOption == ContactSortOption.name,
), ),
@@ -264,22 +252,22 @@ class DiscoveryContactsFilterMenu extends StatelessWidget {
title: l10n.listFilter_filters, title: l10n.listFilter_filters,
options: [ options: [
SortFilterMenuOption( SortFilterMenuOption(
value: _actionFilterAll, value: _DiscoveryTypeFilterAction(ContactTypeFilter.all),
label: l10n.listFilter_all, label: l10n.listFilter_all,
checked: typeFilter == ContactTypeFilter.all, checked: typeFilter == ContactTypeFilter.all,
), ),
SortFilterMenuOption( SortFilterMenuOption(
value: _actionFilterUsers, value: _DiscoveryTypeFilterAction(ContactTypeFilter.users),
label: l10n.listFilter_users, label: l10n.listFilter_users,
checked: typeFilter == ContactTypeFilter.users, checked: typeFilter == ContactTypeFilter.users,
), ),
SortFilterMenuOption( SortFilterMenuOption(
value: _actionFilterRepeaters, value: _DiscoveryTypeFilterAction(ContactTypeFilter.repeaters),
label: l10n.listFilter_repeaters, label: l10n.listFilter_repeaters,
checked: typeFilter == ContactTypeFilter.repeaters, checked: typeFilter == ContactTypeFilter.repeaters,
), ),
SortFilterMenuOption( SortFilterMenuOption(
value: _actionFilterRooms, value: _DiscoveryTypeFilterAction(ContactTypeFilter.rooms),
label: l10n.listFilter_roomServers, label: l10n.listFilter_roomServers,
checked: typeFilter == ContactTypeFilter.rooms, checked: typeFilter == ContactTypeFilter.rooms,
), ),
@@ -288,27 +276,10 @@ class DiscoveryContactsFilterMenu extends StatelessWidget {
], ],
onSelected: (action) { onSelected: (action) {
switch (action) { switch (action) {
case _actionSortName: case _DiscoverySortAction(:final option):
onSortChanged(ContactSortOption.name); onSortChanged(option);
break; case _DiscoveryTypeFilterAction(:final filter):
case _actionSortLastSeen: onTypeFilterChanged(filter);
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;
} }
}, },
); );
+15 -4
View File
@@ -33,11 +33,22 @@ class _PathManagementDialog extends StatefulWidget {
class _PathManagementDialogState extends State<_PathManagementDialog> { class _PathManagementDialogState extends State<_PathManagementDialog> {
bool _showAllPaths = false; bool _showAllPaths = false;
int _resolveContactIndex = -1;
Contact _resolveContact(MeshCoreConnector connector) { Contact _resolveContact(MeshCoreConnector connector) {
return connector.contacts.firstWhere( if (_resolveContactIndex >= 0 &&
_resolveContactIndex < connector.contacts.length &&
connector.contacts[_resolveContactIndex].publicKeyHex ==
widget.contact.publicKeyHex) {
return connector.contacts[_resolveContactIndex];
}
_resolveContactIndex = connector.contacts.indexWhere(
(c) => c.publicKeyHex == widget.contact.publicKeyHex, (c) => c.publicKeyHex == widget.contact.publicKeyHex,
orElse: () => widget.contact,
); );
if (_resolveContactIndex == -1) {
return widget.contact;
}
return connector.contacts[_resolveContactIndex];
} }
String _formatRelativeTime(BuildContext context, DateTime time) { String _formatRelativeTime(BuildContext context, DateTime time) {
@@ -78,7 +89,7 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
builder: (context) => PathTraceMapScreen( builder: (context) => PathTraceMapScreen(
title: context.l10n.contacts_repeaterPathTrace, title: context.l10n.contacts_repeaterPathTrace,
path: Uint8List.fromList(pathBytes), path: Uint8List.fromList(pathBytes),
flipPathRound: true, flipPathAround: true,
targetContact: widget.contact, targetContact: widget.contact,
), ),
), ),
@@ -107,7 +118,7 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
} }
final pathForInput = currentContact.pathIdList; final pathForInput = currentContact.pathIdList;
final availableContacts = connector.contacts final availableContacts = connector.allContacts
.where((c) => c.publicKeyHex != currentContact.publicKeyHex) .where((c) => c.publicKeyHex != currentContact.publicKeyHex)
.toList(); .toList();
+2 -1
View File
@@ -1,5 +1,6 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../models/contact.dart'; import '../models/contact.dart';
@@ -65,7 +66,7 @@ class _PathSelectionDialogState extends State<PathSelectionDialog> {
void _filterValidContacts() { void _filterValidContacts() {
_validContacts = widget.availableContacts _validContacts = widget.availableContacts
.where((c) => c.type == 2 || c.type == 3) .where((c) => c.type == advTypeRepeater || c.type == advTypeRoom)
.toList(); .toList();
} }
+12 -2
View File
@@ -69,11 +69,21 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
bool _isLoggingIn = false; bool _isLoggingIn = false;
int _resolveRepeaterIndex = -1;
Contact _resolveRepeater(MeshCoreConnector connector) { Contact _resolveRepeater(MeshCoreConnector connector) {
return connector.contacts.firstWhere( if (_resolveRepeaterIndex >= 0 &&
_resolveRepeaterIndex < connector.contacts.length &&
connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
widget.repeater.publicKeyHex) {
return connector.contacts[_resolveRepeaterIndex];
}
_resolveRepeaterIndex = connector.contacts.indexWhere(
(c) => c.publicKeyHex == widget.repeater.publicKeyHex, (c) => c.publicKeyHex == widget.repeater.publicKeyHex,
orElse: () => widget.repeater,
); );
if (_resolveRepeaterIndex == -1) {
return widget.repeater;
}
return connector.contacts[_resolveRepeaterIndex];
} }
Future<void> _handleLogin() async { Future<void> _handleLogin() async {
+13 -2
View File
@@ -64,11 +64,22 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
bool _isLoggingIn = false; bool _isLoggingIn = false;
int _resolveRepeaterIndex = -1;
Contact _resolveRepeater(MeshCoreConnector connector) { Contact _resolveRepeater(MeshCoreConnector connector) {
return connector.contacts.firstWhere( if (_resolveRepeaterIndex >= 0 &&
_resolveRepeaterIndex < connector.contacts.length &&
connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
widget.room.publicKeyHex) {
return connector.contacts[_resolveRepeaterIndex];
}
_resolveRepeaterIndex = connector.contacts.indexWhere(
(c) => c.publicKeyHex == widget.room.publicKeyHex, (c) => c.publicKeyHex == widget.room.publicKeyHex,
orElse: () => widget.room,
); );
if (_resolveRepeaterIndex == -1) {
return widget.room;
}
return connector.contacts[_resolveRepeaterIndex];
} }
Future<void> _handleLogin() async { Future<void> _handleLogin() async {
+1 -4
View File
@@ -157,10 +157,7 @@ class _SNRIndicatorState extends State<SNRIndicator> {
repeater.snr, repeater.snr,
widget.connector.currentSf, widget.connector.currentSf,
); );
final allContacts = [ final allContacts = widget.connector.allContacts;
...widget.connector.contacts,
...widget.connector.discoveredContacts,
];
final name = allContacts final name = allContacts
.where((c) => c.publicKey.first == repeater.pubkeyFirstByte) .where((c) => c.publicKey.first == repeater.pubkeyFirstByte)
.map((c) => c.name) .map((c) => c.name)
+1 -1
View File
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 6.0.0+7 version: 7.0.0+8
environment: environment:
sdk: ^3.9.2 sdk: ^3.9.2
+8 -2
View File
@@ -6,6 +6,7 @@ import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/l10n/app_localizations.dart'; import 'package:meshcore_open/l10n/app_localizations.dart';
import 'package:meshcore_open/screens/scanner_screen.dart'; import 'package:meshcore_open/screens/scanner_screen.dart';
import 'package:meshcore_open/screens/tcp_screen.dart'; import 'package:meshcore_open/screens/tcp_screen.dart';
import 'package:meshcore_open/services/app_settings_service.dart';
class _FakeMeshCoreConnector extends MeshCoreConnector { class _FakeMeshCoreConnector extends MeshCoreConnector {
_FakeMeshCoreConnector(); _FakeMeshCoreConnector();
@@ -44,8 +45,13 @@ Widget _buildTestApp({
required Widget child, required Widget child,
Locale? locale, Locale? locale,
}) { }) {
return ChangeNotifierProvider<MeshCoreConnector>.value( return MultiProvider(
value: connector, providers: [
ChangeNotifierProvider<MeshCoreConnector>.value(value: connector),
ChangeNotifierProvider<AppSettingsService>(
create: (_) => AppSettingsService(),
),
],
child: MaterialApp( child: MaterialApp(
locale: locale, locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,