feat(localization): update contact settings translations for multiple languages

- Translated contact settings and related strings in Slovenian, Swedish, Ukrainian, Chinese, Dutch, Polish, Portuguese, Russian, and Slovak.
- Added new strings for discovered contacts actions such as adding, copying, and deleting contacts.
- Enhanced the DiscoveryContact model to include a rawPacket field for better data handling.
- Updated the contacts screen to support new actions in the context menu for discovered contacts.
- Improved the contact discovery store to handle the serialization of the new rawPacket field.
This commit is contained in:
Winston Lowe
2026-03-01 10:13:17 -08:00
parent 92d8e7cd0b
commit 12bf46bba1
37 changed files with 1034 additions and 338 deletions
+91 -6
View File
@@ -283,6 +283,7 @@ class MeshCoreConnector extends ChangeNotifier {
int? get batteryMillivolts => _batteryMillivolts;
int get maxContacts => _maxContacts;
int get maxChannels => _maxChannels;
Set<String> get knownContactKeys => Set.unmodifiable(_knownContactKeys);
bool get isSyncingQueuedMessages => _isSyncingQueuedMessages;
bool get isSyncingChannels => _isSyncingChannels;
int get channelSyncProgress =>
@@ -1534,6 +1535,50 @@ class MeshCoreConnector extends ChangeNotifier {
notifyListeners();
}
Future<void> removeDiscoveredContact(DiscoveryContact contact) async {
if (!isConnected) return;
_discoveredContacts.removeWhere(
(c) => c.publicKeyHex == contact.publicKeyHex,
);
unawaited(_persistDiscoveredContacts());
notifyListeners();
}
Future<void> importDiscoveredContact(DiscoveryContact contact) async {
if (!isConnected) return;
await sendFrame(
buildSetAutoAddConfigFrame(
autoAddChat: true,
autoAddRepeater: true,
autoAddRoomServer: true,
autoAddSensor: true,
overwriteOldest: _overwriteOldest,
),
);
await sendFrame(buildImportContactFrame(contact.rawPacket));
await sendFrame(
buildSetAutoAddConfigFrame(
autoAddChat: _autoAddUsers,
autoAddRepeater: _autoAddRepeaters,
autoAddRoomServer: _autoAddRoomServers,
autoAddSensor: _autoAddSensors,
overwriteOldest: _overwriteOldest,
),
);
_handleContactAdvert(
Contact(
publicKey: contact.publicKey,
name: contact.name,
type: contact.type,
pathLength: contact.pathLength,
path: contact.path,
lastSeen: DateTime.now(),
),
);
notifyListeners();
}
Future<void> clearContactPath(Contact contact) async {
if (!isConnected) return;
@@ -3705,22 +3750,29 @@ class MeshCoreConnector extends ChangeNotifier {
appLogger.warn('Malformed RX frame: $e', tag: 'Connector');
return;
}
final rawPacket = frame.sublist(3);
switch (payloadType) {
case payloadTypeADVERT:
_handlePayloadAdvertReceived(payload, pathBytes, routeType, snr);
_handlePayloadAdvertReceived(
rawPacket,
payload,
pathBytes,
routeType,
snr,
);
break;
default:
}
}
void _handlePayloadAdvertReceived(
Uint8List frame,
Uint8List rawPacket,
Uint8List payload,
Uint8List path,
int routeType,
double snr,
) {
final advert = BufferReader(frame);
final advert = BufferReader(payload);
double latitude = 0.0;
double longitude = 0.0;
String name = '';
@@ -3785,7 +3837,7 @@ class MeshCoreConnector extends ChangeNotifier {
(_autoAddSensors && type == advTypeSensor)) {
_handleContactAdvert(newContact);
} else {
_handleDiscovery(newContact);
_handleDiscovery(newContact, rawPacket);
}
_updateDirectRepeater(newContact, snr, path);
return;
@@ -3890,9 +3942,42 @@ class MeshCoreConnector extends ChangeNotifier {
}
}
void _handleDiscovery(Contact contact) {
void _handleDiscovery(Contact contact, Uint8List rawPacket) {
debugPrint('Discovered new contact: ${contact.name}');
final existingIndex = _discoveredContacts.indexWhere(
(c) => c.publicKeyHex == contact.publicKeyHex,
);
final existingContactsIndex = _contacts.indexWhere(
(c) => c.publicKeyHex == contact.publicKeyHex,
);
if (existingContactsIndex >= 0) {
if (existingIndex >= 0) {
removeDiscoveredContact(_discoveredContacts[existingIndex]);
unawaited(_persistDiscoveredContacts());
}
return;
}
// Update existing contact
if (existingIndex >= 0) {
_discoveredContacts[existingIndex] = _discoveredContacts[existingIndex]
.copyWith(
rawPacket: rawPacket,
name: contact.name,
type: contact.type,
pathLength: contact.pathLength,
path: contact.path,
latitude: contact.latitude,
longitude: contact.longitude,
lastSeen: contact.lastSeen,
);
return;
}
final disContact = DiscoveryContact(
rawPacket: rawPacket,
publicKey: contact.publicKey,
name: contact.name,
type: contact.type,
+20 -18
View File
@@ -114,25 +114,27 @@ class BufferWriter {
}
void writeHex(String hex) {
// Validate hex string length is even and not empty
if (hex.isEmpty || hex.length % 2 != 0) {
throw FormatException('Invalid hex string length: ${hex.length}');
}
List<int> result = [];
for (int i = 0; i < hex.length ~/ 2; i++) {
final hexByte = hex.substring(i * 2, i * 2 + 2);
final byte = int.tryParse(hexByte, radix: 16);
if (byte == null) {
throw FormatException(
'Invalid hex characters at position $i: $hexByte',
);
}
result.add(byte);
}
writeBytes(Uint8List.fromList(result));
writeBytes(hex2Uint8List(hex));
}
}
Uint8List hex2Uint8List(String hex) {
// Validate hex string length is even and not empty
if (hex.isEmpty || hex.length % 2 != 0) {
throw FormatException('Invalid hex string length: ${hex.length}');
}
List<int> result = [];
for (int i = 0; i < hex.length ~/ 2; i++) {
final hexByte = hex.substring(i * 2, i * 2 + 2);
final byte = int.tryParse(hexByte, radix: 16);
if (byte == null) {
throw FormatException('Invalid hex characters at position $i: $hexByte');
}
result.add(byte);
}
return Uint8List.fromList(result);
}
// Command codes (to device)
const int cmdAppStart = 1;
const int cmdSendTxtMsg = 2;
@@ -827,10 +829,10 @@ Uint8List buildExportContactFrame(Uint8List pubKey) {
// Build a import contact frame
// [cmd][contact_frame x98+]
Uint8List buildImportContactFrame(String contactFrame) {
Uint8List buildImportContactFrame(Uint8List contactFrame) {
final writer = BufferWriter();
writer.writeByte(cmdImportContact);
writer.writeHex(contactFrame);
writer.writeBytes(contactFrame);
return writer.toBytes();
}