mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Replace hardcoded English strings in 32 Dart source files with l10n references. Covers settings labels, UI actions, map/SAR features, device config, notifications, profiles, spectrum scan, and status messages for sl, de, hr, pl, es, fr, it, el, ru, tr, uk, zh, pt.
262 lines
7.9 KiB
Dart
262 lines
7.9 KiB
Dart
export 'package:meshcore_client/meshcore_client.dart'
|
|
show Contact, ContactType, ContactTelemetry, AdvertLocation;
|
|
|
|
import 'dart:math' as math;
|
|
import 'dart:typed_data';
|
|
import 'package:flutter/material.dart';
|
|
import '../l10n/app_localizations.dart';
|
|
import 'package:meshcore_client/meshcore_client.dart';
|
|
|
|
class ParsedContactRoute {
|
|
final String canonicalText;
|
|
final int hashSize;
|
|
final int hopCount;
|
|
final int encodedPathLen;
|
|
final int signedEncodedPathLen;
|
|
final Uint8List pathBytes;
|
|
final Uint8List paddedPathBytes;
|
|
|
|
const ParsedContactRoute({
|
|
required this.canonicalText,
|
|
required this.hashSize,
|
|
required this.hopCount,
|
|
required this.encodedPathLen,
|
|
required this.signedEncodedPathLen,
|
|
required this.pathBytes,
|
|
required this.paddedPathBytes,
|
|
});
|
|
|
|
int get byteLength => pathBytes.length;
|
|
String get summary => hopCount == 0
|
|
? 'Direct'
|
|
: '$hopCount hop${hopCount == 1 ? '' : 's'} via $hashSize-byte hashes';
|
|
}
|
|
|
|
class ContactRouteFormatException implements Exception {
|
|
final String message;
|
|
|
|
const ContactRouteFormatException(this.message);
|
|
|
|
@override
|
|
String toString() => message;
|
|
}
|
|
|
|
class ContactRouteCodec {
|
|
static const int maxHashSize = 3;
|
|
static const int maxPathBytes = 64;
|
|
static const int _unknownDescriptor = 0xFF;
|
|
|
|
static ParsedContactRoute parse(String input, {int? expectedHashSize}) {
|
|
final normalized = input.trim().toUpperCase();
|
|
if (normalized.isEmpty) {
|
|
throw const ContactRouteFormatException('Route cannot be empty.');
|
|
}
|
|
|
|
if (expectedHashSize != null &&
|
|
(expectedHashSize < 1 || expectedHashSize > maxHashSize)) {
|
|
throw const ContactRouteFormatException(
|
|
'Hash size must be 1, 2, or 3 bytes.',
|
|
);
|
|
}
|
|
|
|
final hopTokens = normalized
|
|
.split(',')
|
|
.map((token) => token.trim())
|
|
.toList();
|
|
if (hopTokens.any((token) => token.isEmpty)) {
|
|
throw const ContactRouteFormatException('Route contains an empty hop.');
|
|
}
|
|
|
|
final hopBytes = <List<int>>[];
|
|
int? hashSize;
|
|
for (final token in hopTokens) {
|
|
final compact = token.replaceAll(':', '');
|
|
if (compact.isEmpty || !RegExp(r'^[0-9A-F]+$').hasMatch(compact)) {
|
|
throw ContactRouteFormatException('Invalid hop "$token".');
|
|
}
|
|
if (compact.length.isOdd) {
|
|
throw ContactRouteFormatException(
|
|
'Hop "$token" must contain full bytes.',
|
|
);
|
|
}
|
|
|
|
final currentHashSize = compact.length ~/ 2;
|
|
if (currentHashSize < 1 || currentHashSize > maxHashSize) {
|
|
throw ContactRouteFormatException(
|
|
'Hop "$token" must be 1, 2, or 3 bytes.',
|
|
);
|
|
}
|
|
|
|
if (expectedHashSize != null && currentHashSize != expectedHashSize) {
|
|
throw ContactRouteFormatException(
|
|
'Hop "$token" must be $expectedHashSize '
|
|
'byte${expectedHashSize == 1 ? '' : 's'}.',
|
|
);
|
|
}
|
|
|
|
hashSize ??= currentHashSize;
|
|
if (hashSize != currentHashSize) {
|
|
throw const ContactRouteFormatException(
|
|
'All hops in a route must use the same hash size.',
|
|
);
|
|
}
|
|
|
|
final bytes = <int>[];
|
|
for (var i = 0; i < compact.length; i += 2) {
|
|
bytes.add(int.parse(compact.substring(i, i + 2), radix: 16));
|
|
}
|
|
hopBytes.add(bytes);
|
|
}
|
|
|
|
final resolvedHashSize = hashSize ?? 1;
|
|
final flatBytes = Uint8List.fromList(
|
|
hopBytes.expand((hop) => hop).toList(),
|
|
);
|
|
if (flatBytes.length > maxPathBytes) {
|
|
throw const ContactRouteFormatException(
|
|
'Route exceeds the 64-byte firmware limit.',
|
|
);
|
|
}
|
|
|
|
final encodedPathLen =
|
|
((resolvedHashSize - 1) << 6) | (hopBytes.length & 0x3F);
|
|
final padded = Uint8List(maxPathBytes);
|
|
padded.setRange(0, flatBytes.length, flatBytes);
|
|
|
|
return ParsedContactRoute(
|
|
canonicalText: hopBytes
|
|
.map(
|
|
(hop) => hop
|
|
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
|
|
.join()
|
|
.toUpperCase(),
|
|
)
|
|
.join(','),
|
|
hashSize: resolvedHashSize,
|
|
hopCount: hopBytes.length,
|
|
encodedPathLen: encodedPathLen,
|
|
signedEncodedPathLen: toSignedDescriptor(encodedPathLen),
|
|
pathBytes: flatBytes,
|
|
paddedPathBytes: padded,
|
|
);
|
|
}
|
|
|
|
static ParsedContactRoute direct({int hashSize = 1}) {
|
|
if (hashSize < 1 || hashSize > maxHashSize) {
|
|
throw ArgumentError.value(hashSize, 'hashSize', 'Must be 1, 2, or 3.');
|
|
}
|
|
|
|
final encodedPathLen = (hashSize - 1) << 6;
|
|
return ParsedContactRoute(
|
|
canonicalText: '',
|
|
hashSize: hashSize,
|
|
hopCount: 0,
|
|
encodedPathLen: encodedPathLen,
|
|
signedEncodedPathLen: toSignedDescriptor(encodedPathLen),
|
|
pathBytes: Uint8List(0),
|
|
paddedPathBytes: Uint8List(maxPathBytes),
|
|
);
|
|
}
|
|
|
|
static ParsedContactRoute? fromContact(Contact contact) {
|
|
if (!contact.routeHasPath || contact.routeHopCount == 0) {
|
|
return null;
|
|
}
|
|
|
|
return ParsedContactRoute(
|
|
canonicalText: contact.routeCanonicalText,
|
|
hashSize: contact.routeHashSize,
|
|
hopCount: contact.routeHopCount,
|
|
encodedPathLen: contact.routeEncodedPathLen,
|
|
signedEncodedPathLen: contact.routeSignedPathLen,
|
|
pathBytes: contact.routePathBytes,
|
|
paddedPathBytes: _padPath(contact.routePathBytes),
|
|
);
|
|
}
|
|
|
|
static Uint8List _padPath(Uint8List bytes) {
|
|
final padded = Uint8List(maxPathBytes);
|
|
padded.setRange(0, math.min(bytes.length, maxPathBytes), bytes);
|
|
return padded;
|
|
}
|
|
|
|
static int toSignedDescriptor(int encodedPathLen) =>
|
|
encodedPathLen > 127 ? encodedPathLen - 256 : encodedPathLen;
|
|
|
|
static int toUnsignedDescriptor(int signedPathLen) => signedPathLen & 0xFF;
|
|
|
|
static bool isUnknownDescriptor(int signedPathLen) =>
|
|
toUnsignedDescriptor(signedPathLen) == _unknownDescriptor;
|
|
|
|
static bool isValidDescriptor(int signedPathLen) {
|
|
final raw = toUnsignedDescriptor(signedPathLen);
|
|
if (raw == _unknownDescriptor) return false;
|
|
final hashSize = ((raw >> 6) + 1);
|
|
if (hashSize > maxHashSize) return false;
|
|
final hopCount = raw & 0x3F;
|
|
return hopCount * hashSize <= maxPathBytes;
|
|
}
|
|
}
|
|
|
|
extension ContactLocalization on Contact {
|
|
/// Returns the localized display name for special contacts (e.g. Public Channel).
|
|
/// For all other contacts, returns [displayName].
|
|
String getLocalizedDisplayName(BuildContext context) {
|
|
if (isPublicChannel) {
|
|
return AppLocalizations.of(context)!.publicChannel;
|
|
}
|
|
return displayName;
|
|
}
|
|
|
|
int get routeEncodedPathLen =>
|
|
ContactRouteCodec.toUnsignedDescriptor(outPathLen);
|
|
|
|
int get routeSignedPathLen =>
|
|
ContactRouteCodec.toSignedDescriptor(routeEncodedPathLen);
|
|
|
|
bool get routeIsUnknown => ContactRouteCodec.isUnknownDescriptor(outPathLen);
|
|
|
|
bool get routeHasPath =>
|
|
!routeIsUnknown && ContactRouteCodec.isValidDescriptor(outPathLen);
|
|
|
|
int get routeHashSize => routeHasPath ? ((routeEncodedPathLen >> 6) + 1) : 1;
|
|
|
|
int get routeHopCount => routeHasPath ? (routeEncodedPathLen & 0x3F) : -1;
|
|
|
|
int get routeByteLength => routeHasPath
|
|
? math.min(routeHopCount * routeHashSize, outPath.length)
|
|
: 0;
|
|
|
|
Uint8List get routePathBytes => routeByteLength <= 0
|
|
? Uint8List(0)
|
|
: Uint8List.fromList(outPath.sublist(0, routeByteLength));
|
|
|
|
String get routeCanonicalText {
|
|
if (!routeHasPath || routeHopCount <= 0) return '';
|
|
final bytes = routePathBytes;
|
|
final hops = <String>[];
|
|
for (var i = 0; i < bytes.length; i += routeHashSize) {
|
|
hops.add(
|
|
bytes
|
|
.sublist(i, i + routeHashSize)
|
|
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
|
|
.join()
|
|
.toUpperCase(),
|
|
);
|
|
}
|
|
return hops.join(',');
|
|
}
|
|
|
|
String get routeSummary {
|
|
if (routeIsUnknown) {
|
|
return 'Flood/Unknown';
|
|
}
|
|
if (!routeHasPath || routeHopCount == 0) {
|
|
return 'Direct';
|
|
}
|
|
return '$routeHopCount hop${routeHopCount == 1 ? '' : 's'} via $routeHashSize-byte hashes';
|
|
}
|
|
|
|
bool get routeSupportsLegacyRawTransport => routeHasPath;
|
|
}
|