mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-04-20 22:13:48 +00:00
Merge branch 'main' into fix/linux-ble-pairing-flow
This commit is contained in:
commit
7de07c023f
80 changed files with 23673 additions and 1555 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -58,6 +58,7 @@ secrets.dart
|
|||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
macos/Flutter/GeneratedPluginRegistrant.swift
|
||||
|
||||
# iOS
|
||||
**/ios/Pods/
|
||||
|
|
@ -85,4 +86,4 @@ keystore.properties
|
|||
.vscode/settings.json
|
||||
|
||||
# Cloudflare Wrangler
|
||||
.wrangler
|
||||
.wrangler
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:crypto/crypto.dart' as crypto;
|
||||
import 'package:pointycastle/export.dart';
|
||||
|
|
@ -9,6 +10,7 @@ import 'package:flutter_blue_plus_platform_interface/flutter_blue_plus_platform_
|
|||
|
||||
import '../models/channel.dart';
|
||||
import '../models/channel_message.dart';
|
||||
import '../models/companion_radio_stats.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../models/message.dart';
|
||||
import '../models/path_selection.dart';
|
||||
|
|
@ -145,6 +147,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
Timer? _selfInfoRetryTimer;
|
||||
Timer? _reconnectTimer;
|
||||
Timer? _batteryPollTimer;
|
||||
Timer? _radioStatsPollTimer;
|
||||
int _radioStatsPollRefCount = 0;
|
||||
final ValueNotifier<CompanionRadioStats?> radioStatsNotifier =
|
||||
ValueNotifier<CompanionRadioStats?>(null);
|
||||
int _reconnectAttempts = 0;
|
||||
bool _notifyListenersDirty = false;
|
||||
static const Duration _notifyListenersDebounce = Duration(milliseconds: 50);
|
||||
|
|
@ -162,6 +168,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
int? _currentCr;
|
||||
bool? _clientRepeat;
|
||||
int? _firmwareVerCode;
|
||||
int _pathHashByteWidth = 1;
|
||||
CompanionRadioStats? _latestRadioStats;
|
||||
Stopwatch? _airtimeBumpStopwatch;
|
||||
int _prevTotalAirSecs = 0;
|
||||
int? _batteryMillivolts;
|
||||
double? _selfLatitude;
|
||||
double? _selfLongitude;
|
||||
|
|
@ -175,9 +185,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
DateTime _lastRxTime = DateTime.now();
|
||||
DateTime _lastRadioRxTime = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
DateTime _lastContactMsgRxTime = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
DateTime _lastChannelMsgRxTime = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
static const int _radioQuietMs = 3000;
|
||||
static const int _radioQuietMaxWaitMs = 3000;
|
||||
static const int _contactMsgBackoffMs = 5000;
|
||||
|
||||
/// When companion radio stats are unavailable, keep the legacy fixed backoff.
|
||||
static const int _contactMsgBackoffFallbackMs = 5000;
|
||||
static const int _contactMsgBackoffMinMs = 500;
|
||||
static const int _contactMsgBackoffMaxMs = 15000;
|
||||
bool _batteryRequested = false;
|
||||
bool _awaitingSelfInfo = false;
|
||||
bool _hasReceivedDeviceInfo = false;
|
||||
|
|
@ -259,6 +274,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
int? _activeChannelIndex;
|
||||
List<int> _channelOrder = [];
|
||||
|
||||
int _storageUsedKb = -1;
|
||||
int _storageTotalKb = -1;
|
||||
|
||||
// Getters
|
||||
MeshCoreConnectionState get state => _state;
|
||||
BluetoothDevice? get device => _device;
|
||||
|
|
@ -324,6 +342,19 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
List<DirectRepeater> get directRepeaters => _directRepeaters;
|
||||
int? get currentTxPower => _currentTxPower;
|
||||
int? get maxTxPower => _maxTxPower;
|
||||
|
||||
int get pathHashByteWidth => _pathHashByteWidth;
|
||||
|
||||
CompanionRadioStats? get latestRadioStats => _latestRadioStats;
|
||||
|
||||
bool get supportsCompanionRadioStats => (_firmwareVerCode ?? 0) >= 8;
|
||||
|
||||
bool get radioStatsAirActivityPulse {
|
||||
final sw = _airtimeBumpStopwatch;
|
||||
if (sw == null || !sw.isRunning) return false;
|
||||
return sw.elapsed < const Duration(seconds: 2);
|
||||
}
|
||||
|
||||
int? get currentFreqHz => _currentFreqHz;
|
||||
int? get currentBwHz => _currentBwHz;
|
||||
int? get currentSf => _currentSf;
|
||||
|
|
@ -333,10 +364,17 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
bool? get autoAddRoomServers => _autoAddRoomServers;
|
||||
bool? get autoAddSensors => _autoAddSensors;
|
||||
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;
|
||||
int? get firmwareVerCode => _firmwareVerCode;
|
||||
Map<String, String>? get currentCustomVars => _currentCustomVars;
|
||||
int? get batteryMillivolts => _batteryMillivolts;
|
||||
int? get storageUsedKb => _storageUsedKb;
|
||||
int? get storageTotalKb => _storageTotalKb;
|
||||
int get maxContacts => _maxContacts;
|
||||
int get maxChannels => _maxChannels;
|
||||
Set<String> get knownContactKeys => Set.unmodifiable(_knownContactKeys);
|
||||
|
|
@ -773,15 +811,70 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _waitForRadioQuiet() async {
|
||||
// Wait for backoff after receiving a contact message (avoid collision
|
||||
// with their transmission still propagating through repeaters)
|
||||
final msSinceContactMsg = DateTime.now()
|
||||
.difference(_lastContactMsgRxTime)
|
||||
.inMilliseconds;
|
||||
if (msSinceContactMsg < _contactMsgBackoffMs) {
|
||||
final waitMs = _contactMsgBackoffMs - msSinceContactMsg;
|
||||
debugPrint('Contact message backoff: waiting ${waitMs}ms');
|
||||
/// After an incoming DM or channel message, wait before TX so we do not
|
||||
/// collide with mesh propagation. With companion stats, scale wait by RF
|
||||
/// conditions (up to [_contactMsgBackoffMaxMs]); otherwise use
|
||||
/// [_contactMsgBackoffFallbackMs].
|
||||
int _contactMessageBackoffTargetMs() {
|
||||
if (!supportsCompanionRadioStats || _latestRadioStats == null) {
|
||||
return _contactMsgBackoffFallbackMs;
|
||||
}
|
||||
final stats = _latestRadioStats!;
|
||||
final nf = stats.noiseFloorDbm.toDouble();
|
||||
// Quieter (more negative) → lower score; noisier → higher.
|
||||
const noiseQuietDbm = -118.0;
|
||||
const noiseNoisyDbm = -88.0;
|
||||
final noiseT = ((nf - noiseQuietDbm) / (noiseNoisyDbm - noiseQuietDbm))
|
||||
.clamp(0.0, 1.0);
|
||||
|
||||
final snr = stats.lastSnrDb;
|
||||
const snrGood = 12.0;
|
||||
const snrBad = -2.0;
|
||||
final snrT = (1.0 - ((snr - snrBad) / (snrGood - snrBad))).clamp(0.0, 1.0);
|
||||
|
||||
final airBusy = _recentAirtimeBusyFraction();
|
||||
final severity = (math.max(noiseT, snrT) * 0.82 + airBusy * 0.18).clamp(
|
||||
0.0,
|
||||
1.0,
|
||||
);
|
||||
|
||||
return (_contactMsgBackoffMinMs +
|
||||
severity * (_contactMsgBackoffMaxMs - _contactMsgBackoffMinMs))
|
||||
.round();
|
||||
}
|
||||
|
||||
/// 1.0 shortly after TX/RX airtime counters increase, decaying to 0 over ~8s.
|
||||
double _recentAirtimeBusyFraction() {
|
||||
final sw = _airtimeBumpStopwatch;
|
||||
if (sw == null || !sw.isRunning) return 0;
|
||||
final ms = sw.elapsedMilliseconds;
|
||||
const windowMs = 8000;
|
||||
if (ms >= windowMs) return 0;
|
||||
return 1.0 - (ms / windowMs);
|
||||
}
|
||||
|
||||
/// Start of the post-inbound cool-down: the later of BLE message RX time and
|
||||
/// companion airtime bump ([_airtimeBumpStopwatch], same as the activity dot).
|
||||
DateTime _postTxBackoffAnchor(DateTime lastInboundRxTime) {
|
||||
if (!supportsCompanionRadioStats) return lastInboundRxTime;
|
||||
final sw = _airtimeBumpStopwatch;
|
||||
if (sw == null || !sw.isRunning) return lastInboundRxTime;
|
||||
final bumpAt = DateTime.now().subtract(sw.elapsed);
|
||||
return bumpAt.isAfter(lastInboundRxTime) ? bumpAt : lastInboundRxTime;
|
||||
}
|
||||
|
||||
Future<void> _waitForRadioQuiet({required DateTime lastInboundRxTime}) async {
|
||||
// Wait for backoff after inbound traffic / RF airtime (avoid collision with
|
||||
// mesh propagation). Elapsed time uses the dot's airtime bump when newer.
|
||||
final backoffTargetMs = _contactMessageBackoffTargetMs();
|
||||
final anchor = _postTxBackoffAnchor(lastInboundRxTime);
|
||||
final msSinceAnchor = DateTime.now().difference(anchor).inMilliseconds;
|
||||
if (msSinceAnchor < backoffTargetMs) {
|
||||
final waitMs = backoffTargetMs - msSinceAnchor;
|
||||
debugPrint(
|
||||
'Post-inbound backoff: waiting ${waitMs}ms '
|
||||
'(target=${backoffTargetMs}ms, anchorAge=${msSinceAnchor}ms)',
|
||||
);
|
||||
await Future<void>.delayed(Duration(milliseconds: waitMs));
|
||||
}
|
||||
|
||||
|
|
@ -815,7 +908,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
) async {
|
||||
if (!isConnected || text.isEmpty) return;
|
||||
try {
|
||||
await _waitForRadioQuiet();
|
||||
await _waitForRadioQuiet(lastInboundRxTime: _lastContactMsgRxTime);
|
||||
final outboundText = prepareContactOutboundText(contact, text);
|
||||
await sendFrame(
|
||||
buildSendTextMsgFrame(
|
||||
|
|
@ -1154,6 +1247,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
);
|
||||
await _requestDeviceInfo();
|
||||
_startBatteryPolling();
|
||||
if (_radioStatsPollRefCount > 0) _startRadioStatsPolling();
|
||||
var gotSelfInfo = await _waitForSelfInfo(
|
||||
timeout: const Duration(seconds: 3),
|
||||
);
|
||||
|
|
@ -1259,6 +1353,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
_pendingInitialChannelSync = true;
|
||||
await _requestDeviceInfo();
|
||||
_startBatteryPolling();
|
||||
if (_radioStatsPollRefCount > 0) _startRadioStatsPolling();
|
||||
|
||||
var gotSelfInfo = await _waitForSelfInfo(
|
||||
timeout: const Duration(seconds: 3),
|
||||
|
|
@ -1893,6 +1988,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
|
||||
await _requestDeviceInfo();
|
||||
_startBatteryPolling();
|
||||
if (_radioStatsPollRefCount > 0) _startRadioStatsPolling();
|
||||
|
||||
final gotSelfInfo = await _waitForSelfInfo(
|
||||
timeout: const Duration(seconds: 3),
|
||||
|
|
@ -1920,6 +2016,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
_pendingInitialContactsSync = false;
|
||||
_bleInitialSyncStarted = false;
|
||||
_pendingDeferredChannelSyncAfterContacts = false;
|
||||
_pathHashByteWidth = 1;
|
||||
}
|
||||
|
||||
bool get _shouldAutoReconnect =>
|
||||
|
|
@ -1999,6 +2096,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
}
|
||||
_setState(MeshCoreConnectionState.disconnecting);
|
||||
_stopBatteryPolling();
|
||||
_stopRadioStatsPolling();
|
||||
|
||||
await _usbFrameSubscription?.cancel();
|
||||
_usbFrameSubscription = null;
|
||||
|
|
@ -2144,6 +2242,49 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
_batteryPollTimer = null;
|
||||
}
|
||||
|
||||
void _startRadioStatsPolling() {
|
||||
_radioStatsPollTimer?.cancel();
|
||||
_radioStatsPollTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
if (!isConnected) {
|
||||
_stopRadioStatsPolling();
|
||||
return;
|
||||
}
|
||||
unawaited(requestRadioStats());
|
||||
});
|
||||
}
|
||||
|
||||
void _stopRadioStatsPolling() {
|
||||
_radioStatsPollTimer?.cancel();
|
||||
_radioStatsPollTimer = null;
|
||||
}
|
||||
|
||||
void acquireRadioStatsPolling() {
|
||||
_radioStatsPollRefCount++;
|
||||
if (_radioStatsPollRefCount == 1 && isConnected) {
|
||||
_startRadioStatsPolling();
|
||||
}
|
||||
}
|
||||
|
||||
void releaseRadioStatsPolling() {
|
||||
_radioStatsPollRefCount = (_radioStatsPollRefCount - 1).clamp(0, 999);
|
||||
if (_radioStatsPollRefCount == 0) {
|
||||
_stopRadioStatsPolling();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> requestRadioStats() async {
|
||||
if (!isConnected) return;
|
||||
if (!supportsCompanionRadioStats) return;
|
||||
try {
|
||||
await sendFrame(buildGetStatsFrame(statsTypeRadio));
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> setPathHashMode(int mode) async {
|
||||
if (!isConnected) return;
|
||||
await sendFrame(buildSetPathHashModeFrame(mode.clamp(0, 2)));
|
||||
}
|
||||
|
||||
Future<void> refreshDeviceInfo() async {
|
||||
if (!isConnected) return;
|
||||
if (PlatformInfo.isWeb &&
|
||||
|
|
@ -2346,13 +2487,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;
|
||||
final latestContact =
|
||||
await _fetchContactSnapshotFromDevice(contact.publicKey) ?? contact;
|
||||
final updatedFlags = isFavorite
|
||||
? (latestContact.flags | contactFlagFavorite)
|
||||
: (latestContact.flags & ~contactFlagFavorite);
|
||||
int updatedFlags = isFavorite != null
|
||||
? (isFavorite
|
||||
? (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(
|
||||
buildUpdateContactPathFrame(
|
||||
|
|
@ -2518,9 +2682,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
outboundText,
|
||||
selfKey,
|
||||
);
|
||||
final ackHashHex = ackHash
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
final ackHashHex = ackHashToHex(ackHash);
|
||||
final messageBytes = utf8.encode(outboundText).length;
|
||||
_pendingRepeaterAcks[ackHashHex]?.timeout?.cancel();
|
||||
_pendingRepeaterAcks[ackHashHex] = _RepeaterAckContext(
|
||||
|
|
@ -2612,6 +2774,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
// Send the reaction to the device (don't add as a visible message)
|
||||
final reactionQueueId = _nextReactionSendQueueId();
|
||||
_pendingChannelSentQueue.add(reactionQueueId);
|
||||
await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime);
|
||||
await sendFrame(
|
||||
buildSendChannelTextMsgFrame(channel.index, text),
|
||||
channelSendQueueId: reactionQueueId,
|
||||
|
|
@ -2636,6 +2799,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
(isChannelSmazEnabled(channel.index) && !isStructuredPayload)
|
||||
? Smaz.encodeIfSmaller(text)
|
||||
: text;
|
||||
await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime);
|
||||
await sendFrame(
|
||||
buildSendChannelTextMsgFrame(channel.index, outboundText),
|
||||
channelSendQueueId: message.messageId,
|
||||
|
|
@ -2892,6 +3056,31 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
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 {
|
||||
if (!isConnected) return;
|
||||
if (_isSyncingChannels) {
|
||||
|
|
@ -3074,7 +3263,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
_bleDebugLogService?.logFrame(frame, outgoing: false);
|
||||
|
||||
final code = frame[0];
|
||||
debugPrint('RX frame: code=$code len=${frame.length}');
|
||||
// debugPrint('RX frame: code=$code len=${frame.length}');
|
||||
|
||||
switch (code) {
|
||||
case respCodeOk:
|
||||
|
|
@ -3176,6 +3365,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
case respCodeBattAndStorage:
|
||||
_handleBatteryAndStorage(frame);
|
||||
break;
|
||||
case respCodeStats:
|
||||
_handleStatsFrame(frame);
|
||||
break;
|
||||
case respCodeCustomVars:
|
||||
_handleCustomVars(frame);
|
||||
break;
|
||||
|
|
@ -3248,8 +3440,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
final reader = BufferReader(frame);
|
||||
try {
|
||||
reader.skipBytes(2);
|
||||
_currentTxPower = reader.readByte();
|
||||
_maxTxPower = reader.readByte();
|
||||
_currentTxPower = reader.readInt8();
|
||||
_maxTxPower = reader.readInt8();
|
||||
_selfPublicKey = reader.readBytes(pubKeySize);
|
||||
_selfLatitude = reader.readInt32LE() / 1000000.0;
|
||||
_selfLongitude = reader.readInt32LE() / 1000000.0;
|
||||
|
|
@ -3267,7 +3459,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
_currentSf = reader.readByte();
|
||||
_currentCr = reader.readByte();
|
||||
|
||||
_selfName = reader.readString();
|
||||
_selfName = reader.readCString();
|
||||
} catch (e) {
|
||||
_appDebugLogService?.error(
|
||||
'Error parsing SELF_INFO frame: $e',
|
||||
|
|
@ -3343,6 +3535,13 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
if (frame.length >= 81) {
|
||||
_clientRepeat = frame[80] != 0;
|
||||
}
|
||||
// Path hash mode v10+ (byte 81): width = mode + 1 byte(s) per hop
|
||||
if (frame.length >= 82) {
|
||||
final mode = (frame[81] & 0xFF).clamp(0, 2);
|
||||
_pathHashByteWidth = mode + 1;
|
||||
} else {
|
||||
_pathHashByteWidth = 1;
|
||||
}
|
||||
|
||||
// Firmware reports MAX_CONTACTS / 2 for v3+ device info.
|
||||
final reportedContacts = frame[2];
|
||||
|
|
@ -3402,20 +3601,42 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
unawaited(_requestNextQueuedMessage());
|
||||
}
|
||||
|
||||
void _handleStatsFrame(Uint8List frame) {
|
||||
final stats = CompanionRadioStats.tryParse(frame);
|
||||
if (stats == null) return;
|
||||
final total = stats.txAirSecs + stats.rxAirSecs;
|
||||
if (total > _prevTotalAirSecs) {
|
||||
(_airtimeBumpStopwatch ??= Stopwatch()).reset();
|
||||
_airtimeBumpStopwatch!.start();
|
||||
}
|
||||
_prevTotalAirSecs = total;
|
||||
_latestRadioStats = stats;
|
||||
radioStatsNotifier.value = stats;
|
||||
}
|
||||
|
||||
void _handleBatteryAndStorage(Uint8List frame) {
|
||||
// Frame format from C++:
|
||||
// [0] = RESP_CODE_BATT_AND_STORAGE
|
||||
// [1-2] = battery_mv (uint16 LE)
|
||||
// [3-6] = storage_used_kb (uint32 LE)
|
||||
// [7-10] = storage_total_kb (uint32 LE)
|
||||
if (frame.length >= 3) {
|
||||
_batteryMillivolts = readUint16LE(frame, 1);
|
||||
try {
|
||||
final reader = BufferReader(frame);
|
||||
reader.skipBytes(1);
|
||||
_batteryMillivolts = reader.readUInt16LE();
|
||||
_storageUsedKb = reader.readUInt32LE();
|
||||
_storageTotalKb = reader.readUInt32LE();
|
||||
final volts = (_batteryMillivolts! / 1000.0).toStringAsFixed(2);
|
||||
_appDebugLogService?.info(
|
||||
'Pulled battery: $volts V ($_batteryMillivolts mV)',
|
||||
tag: 'Battery',
|
||||
);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_appDebugLogService?.error(
|
||||
'Error parsing battery and storage frame: $e',
|
||||
tag: 'Connector',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3761,9 +3982,10 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
}
|
||||
|
||||
bool _pathMatchesContact(Uint8List pathBytes, Uint8List publicKey) {
|
||||
if (pathBytes.isEmpty || publicKey.length < pathHashSize) return false;
|
||||
for (int i = 0; i + pathHashSize <= pathBytes.length; i += pathHashSize) {
|
||||
final prefix = pathBytes.sublist(i, i + pathHashSize);
|
||||
final w = _pathHashByteWidth;
|
||||
if (pathBytes.isEmpty || publicKey.length < w) return false;
|
||||
for (int i = 0; i + w <= pathBytes.length; i += w) {
|
||||
final prefix = pathBytes.sublist(i, i + w);
|
||||
if (_matchesPrefix(publicKey, prefix)) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -3911,7 +4133,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
reader.skipBytes(4); // Skip extra 4 bytes for signed/plain variants
|
||||
}
|
||||
|
||||
final msgText = reader.readString();
|
||||
final msgText = reader.readCString();
|
||||
|
||||
final flags = txtType;
|
||||
final shiftedType = flags >> 2;
|
||||
|
|
@ -4048,6 +4270,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
if (_shouldDropSelfChannelMessage(parsed.senderName, parsed.pathBytes)) {
|
||||
return;
|
||||
}
|
||||
_lastChannelMsgRxTime = DateTime.now();
|
||||
final contentHash = _computeContentHash(
|
||||
parsed.channelIndex!,
|
||||
parsed.timestamp.millisecondsSinceEpoch ~/ 1000,
|
||||
|
|
@ -4073,68 +4296,87 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
|
||||
void _handleLogRxData(Uint8List frame) {
|
||||
if (frame.length < 4) return;
|
||||
final raw = Uint8List.fromList(frame.sublist(3));
|
||||
final packet = _parseRawPacket(raw);
|
||||
if (packet == null || packet.payloadType != _payloadTypeGroupText) return;
|
||||
try {
|
||||
final reader = BufferReader(frame);
|
||||
reader.skipBytes(3); // Skip header
|
||||
|
||||
final payload = packet.payload;
|
||||
if (payload.length <= _cipherMacSize) return;
|
||||
final channelHash = payload[0];
|
||||
final encrypted = Uint8List.fromList(payload.sublist(1));
|
||||
final raw = reader.readRemainingBytes();
|
||||
final packet = _parseRawPacket(raw);
|
||||
if (packet == null || packet.payloadType != _payloadTypeGroupText) return;
|
||||
|
||||
// Use cached channels as fallback if live channels not yet loaded
|
||||
final channelsToSearch = _channels.isNotEmpty ? _channels : _cachedChannels;
|
||||
for (final channel in channelsToSearch) {
|
||||
if (channel.isEmpty) continue;
|
||||
final hash = _computeChannelHash(channel.psk);
|
||||
if (hash != channelHash) continue;
|
||||
final payload = BufferReader(packet.payload);
|
||||
final channelHash = payload.readByte();
|
||||
final encrypted = Uint8List.fromList(payload.readRemainingBytes());
|
||||
|
||||
final decrypted = _decryptPayload(channel.psk, encrypted);
|
||||
if (decrypted == null || decrypted.length < 6) return;
|
||||
// Use cached channels as fallback if live channels not yet loaded
|
||||
final channelsToSearch = _channels.isNotEmpty
|
||||
? _channels
|
||||
: _cachedChannels;
|
||||
for (final channel in channelsToSearch) {
|
||||
if (channel.isEmpty) continue;
|
||||
final hash = _computeChannelHash(channel.psk);
|
||||
if (hash != channelHash) continue;
|
||||
try {
|
||||
final decryptedBytes = _decryptPayload(channel.psk, encrypted);
|
||||
if (decryptedBytes == null || decryptedBytes.length < 6) return;
|
||||
final decrypted = BufferReader(decryptedBytes);
|
||||
|
||||
final txtType = decrypted[4];
|
||||
if ((txtType >> 2) != 0) {
|
||||
return;
|
||||
final timestampRaw = decrypted.readUInt32LE();
|
||||
final txtType = decrypted.readByte();
|
||||
if ((txtType >> 2) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
final text = decrypted.readCString();
|
||||
final parsed = _splitSenderText(text);
|
||||
final decodedText =
|
||||
Smaz.tryDecodePrefixed(parsed.text) ?? parsed.text;
|
||||
if (_shouldDropSelfChannelMessage(
|
||||
parsed.senderName,
|
||||
packet.pathBytes,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final pktHash = _computePacketHash(
|
||||
packet.payloadType,
|
||||
packet.payload,
|
||||
);
|
||||
|
||||
final message = ChannelMessage(
|
||||
senderKey: null,
|
||||
senderName: parsed.senderName,
|
||||
text: decodedText,
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
|
||||
isOutgoing: false,
|
||||
status: ChannelMessageStatus.sent,
|
||||
pathLength: packet.isFlood ? packet.hopCount : 0,
|
||||
pathBytes: packet.pathBytes,
|
||||
channelIndex: channel.index,
|
||||
packetHash: pktHash,
|
||||
);
|
||||
|
||||
_updateContactLastMessageAtByName(
|
||||
parsed.senderName,
|
||||
message.timestamp,
|
||||
pathBytes: message.pathBytes,
|
||||
);
|
||||
final isNew = _addChannelMessage(channel.index, message);
|
||||
_maybeIncrementChannelUnread(message, isNew: isNew);
|
||||
notifyListeners();
|
||||
if (isNew) {
|
||||
final label = channel.name.isEmpty
|
||||
? 'Channel ${channel.index}'
|
||||
: channel.name;
|
||||
_maybeNotifyChannelMessage(message, channelName: label);
|
||||
}
|
||||
return;
|
||||
} catch (e) {
|
||||
appLogger.warn('Decryption failed for channel ${channel.index}: $e');
|
||||
}
|
||||
}
|
||||
|
||||
final timestampRaw = readUint32LE(decrypted, 0);
|
||||
final text = readCString(decrypted, 5, decrypted.length - 5);
|
||||
final parsed = _splitSenderText(text);
|
||||
final decodedText = Smaz.tryDecodePrefixed(parsed.text) ?? parsed.text;
|
||||
if (_shouldDropSelfChannelMessage(parsed.senderName, packet.pathBytes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final pktHash = _computePacketHash(packet.payloadType, packet.payload);
|
||||
|
||||
final message = ChannelMessage(
|
||||
senderKey: null,
|
||||
senderName: parsed.senderName,
|
||||
text: decodedText,
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
|
||||
isOutgoing: false,
|
||||
status: ChannelMessageStatus.sent,
|
||||
pathLength: packet.isFlood ? packet.hopCount : 0,
|
||||
pathBytes: packet.pathBytes,
|
||||
channelIndex: channel.index,
|
||||
packetHash: pktHash,
|
||||
);
|
||||
|
||||
_updateContactLastMessageAtByName(
|
||||
parsed.senderName,
|
||||
message.timestamp,
|
||||
pathBytes: message.pathBytes,
|
||||
);
|
||||
final isNew = _addChannelMessage(channel.index, message);
|
||||
_maybeIncrementChannelUnread(message, isNew: isNew);
|
||||
notifyListeners();
|
||||
if (isNew) {
|
||||
final label = channel.name.isEmpty
|
||||
? 'Channel ${channel.index}'
|
||||
: channel.name;
|
||||
_maybeNotifyChannelMessage(message, channelName: label);
|
||||
}
|
||||
return;
|
||||
} catch (e) {
|
||||
appLogger.warn('Error handling log RX data frame: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4145,15 +4387,15 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
// [2-5] = expected_ack_hash (uint32)
|
||||
// [6-9] = estimated_timeout_ms (uint32)
|
||||
|
||||
if (frame.length >= 10) {
|
||||
final ackHash = Uint8List.fromList(frame.sublist(2, 6));
|
||||
final timeoutMs = readUint32LE(frame, 6);
|
||||
try {
|
||||
final reader = BufferReader(frame);
|
||||
reader.skipBytes(2); //Skip code and is_flood
|
||||
final ackHash = reader.readUInt32LE();
|
||||
final timeoutMs = reader.readUInt32LE();
|
||||
|
||||
// Check if this is a CLI command ACK - if so, ignore it
|
||||
if (_lastSentWasCliCommand) {
|
||||
final ackHashHex = ackHash
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
final ackHashHex = ackHashToHex(ackHash);
|
||||
debugPrint('Ignoring CLI command ACK (sent): $ackHashHex');
|
||||
_lastSentWasCliCommand = false;
|
||||
return;
|
||||
|
|
@ -4172,7 +4414,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
if (_markNextPendingChannelMessageSent()) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
} catch (e) {
|
||||
appLogger.warn('Error handling message sent frame: $e');
|
||||
// Fallback to old behavior
|
||||
for (var messages in _conversations.values) {
|
||||
for (int i = messages.length - 1; i >= 0; i--) {
|
||||
|
|
@ -4251,9 +4494,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
// [1-4] = ack_hash (uint32)
|
||||
// [5-8] = trip_time_ms (uint32)
|
||||
|
||||
if (frame.length >= 9) {
|
||||
final ackHash = Uint8List.fromList(frame.sublist(1, 5));
|
||||
final tripTimeMs = readUint32LE(frame, 5);
|
||||
try {
|
||||
final reader = BufferReader(frame);
|
||||
reader.skipBytes(1); // Skip code
|
||||
final ackHash = reader.readUInt32LE();
|
||||
final tripTimeMs = reader.readUInt32LE();
|
||||
|
||||
// CLI command ACKs are already filtered in _handleMessageSent, so this should only see real messages
|
||||
|
||||
|
|
@ -4265,7 +4510,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
if (_retryService != null) {
|
||||
_retryService!.handleAckReceived(ackHash, tripTimeMs);
|
||||
}
|
||||
} else {
|
||||
} catch (e) {
|
||||
appLogger.warn('Error handling send confirmed frame: $e');
|
||||
// Fallback to old behavior
|
||||
for (var messages in _conversations.values) {
|
||||
for (int i = messages.length - 1; i >= 0; i--) {
|
||||
|
|
@ -4280,10 +4526,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
}
|
||||
}
|
||||
|
||||
bool _handleRepeaterCommandSent(Uint8List ackHash, int timeoutMs) {
|
||||
final ackHashHex = ackHash
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
bool _handleRepeaterCommandSent(int ackHash, int timeoutMs) {
|
||||
final ackHashHex = ackHashToHex(ackHash);
|
||||
final entry = _pendingRepeaterAcks[ackHashHex];
|
||||
if (entry == null) return false;
|
||||
|
||||
|
|
@ -4301,10 +4545,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
return true;
|
||||
}
|
||||
|
||||
bool _handleRepeaterCommandAck(Uint8List ackHash, int tripTimeMs) {
|
||||
final ackHashHex = ackHash
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
bool _handleRepeaterCommandAck(int ackHash, int tripTimeMs) {
|
||||
final ackHashHex = ackHashToHex(ackHash);
|
||||
final entry = _pendingRepeaterAcks.remove(ackHashHex);
|
||||
if (entry == null) return false;
|
||||
entry.timeout?.cancel();
|
||||
|
|
@ -4655,36 +4897,35 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
}
|
||||
|
||||
_RawPacket? _parseRawPacket(Uint8List raw) {
|
||||
if (raw.length < 3) return null;
|
||||
var index = 0;
|
||||
final header = raw[index++];
|
||||
final routeType = header & _phRouteMask;
|
||||
final hasTransport =
|
||||
routeType == _routeTransportFlood || routeType == _routeTransportDirect;
|
||||
if (hasTransport) {
|
||||
if (raw.length < index + 4) return null;
|
||||
index += 4;
|
||||
}
|
||||
if (raw.length <= index) return null;
|
||||
final pathLenRaw = raw[index++];
|
||||
final pathByteLen = _decodePathByteLen(pathLenRaw);
|
||||
if (raw.length < index + pathByteLen) return null;
|
||||
final pathBytes = Uint8List.fromList(
|
||||
raw.sublist(index, index + pathByteLen),
|
||||
);
|
||||
index += pathByteLen;
|
||||
if (raw.length <= index) return null;
|
||||
final payload = Uint8List.fromList(raw.sublist(index));
|
||||
try {
|
||||
final reader = BufferReader(raw);
|
||||
final header = reader.readByte();
|
||||
final routeType = header & _phRouteMask;
|
||||
final hasTransport =
|
||||
routeType == _routeTransportFlood ||
|
||||
routeType == _routeTransportDirect;
|
||||
if (hasTransport) {
|
||||
// Skip reserved bytes in transport header made up of two u16 fields
|
||||
reader.skipBytes(4);
|
||||
}
|
||||
final pathLenRaw = reader.readByte();
|
||||
final pathByteLen = _decodePathByteLen(pathLenRaw);
|
||||
final pathBytes = reader.readBytes(pathByteLen);
|
||||
final payload = reader.readBytes(reader.remaining);
|
||||
|
||||
return _RawPacket(
|
||||
header: header,
|
||||
routeType: routeType,
|
||||
payloadType: (header >> _phTypeShift) & _phTypeMask,
|
||||
payloadVer: (header >> _phVerShift) & _phVerMask,
|
||||
pathLenRaw: pathLenRaw,
|
||||
pathBytes: pathBytes,
|
||||
payload: payload,
|
||||
);
|
||||
return _RawPacket(
|
||||
header: header,
|
||||
routeType: routeType,
|
||||
payloadType: (header >> _phTypeShift) & _phTypeMask,
|
||||
payloadVer: (header >> _phVerShift) & _phVerMask,
|
||||
pathLenRaw: pathLenRaw,
|
||||
pathBytes: pathBytes,
|
||||
payload: payload,
|
||||
);
|
||||
} catch (e) {
|
||||
appLogger.warn('Error parsing raw packet: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
int _computeChannelHash(Uint8List psk) {
|
||||
|
|
@ -5021,6 +5262,12 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
|
||||
void _handleDisconnection() {
|
||||
_stopBatteryPolling();
|
||||
_stopRadioStatsPolling();
|
||||
_latestRadioStats = null;
|
||||
radioStatsNotifier.value = null;
|
||||
_prevTotalAirSecs = 0;
|
||||
_airtimeBumpStopwatch?.stop();
|
||||
_airtimeBumpStopwatch = null;
|
||||
|
||||
for (final entry in _pendingRepeaterAcks.values) {
|
||||
entry.timeout?.cancel();
|
||||
|
|
@ -5103,7 +5350,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
void _handleCustomVars(Uint8List frame) {
|
||||
final buf = BufferReader(frame.sublist(1));
|
||||
try {
|
||||
_currentCustomVars = _parseKeyValueString(buf.readString());
|
||||
_currentCustomVars = _parseKeyValueString(buf.readCString());
|
||||
} catch (e) {
|
||||
appLogger.warn('Malformed custom vars frame: $e', tag: 'Connector');
|
||||
}
|
||||
|
|
@ -5159,6 +5406,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
_notifyListenersTimer?.cancel();
|
||||
_reconnectTimer?.cancel();
|
||||
_batteryPollTimer?.cancel();
|
||||
_radioStatsPollTimer?.cancel();
|
||||
radioStatsNotifier.dispose();
|
||||
_receivedFramesController.close();
|
||||
_usbManager.dispose();
|
||||
_tcpConnector.dispose();
|
||||
|
|
@ -5260,7 +5509,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
longitude = packet.readInt32LE() / 1e6;
|
||||
}
|
||||
if (hasName && packet.remaining > 0) {
|
||||
name = packet.readString();
|
||||
name = packet.readCString();
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.warn('Malformed advert frame: $e', tag: 'Connector');
|
||||
|
|
@ -5284,6 +5533,17 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
);
|
||||
}
|
||||
|
||||
bool hasValidLocation(double? latitude, double? longitude) {
|
||||
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;
|
||||
}
|
||||
|
||||
void _handlePayloadAdvertReceived(
|
||||
Uint8List rawPacket,
|
||||
Uint8List payload,
|
||||
|
|
@ -5321,8 +5581,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
latitude = advert.readInt32LE() / 1e6;
|
||||
longitude = advert.readInt32LE() / 1e6;
|
||||
}
|
||||
// Validate location values if present
|
||||
hasLocation = hasValidLocation(latitude, longitude);
|
||||
|
||||
if (hasName && advert.remaining > 0) {
|
||||
name = advert.readString();
|
||||
name = advert.readCString();
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.warn('Malformed advert frame: $e', tag: 'Connector');
|
||||
|
|
@ -5386,20 +5649,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
|
||||
// CRITICAL: Preserve user's path override when contact is refreshed from device
|
||||
_contacts[existingIndex] = existing.copyWith(
|
||||
latitude:
|
||||
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,
|
||||
latitude: hasLocation ? latitude : existing.latitude,
|
||||
longitude: hasLocation ? longitude : existing.longitude,
|
||||
name: hasName ? name : existing.name,
|
||||
path: Uint8List.fromList(path.reversed.toList()),
|
||||
pathLength: path.length,
|
||||
|
|
@ -5548,6 +5799,29 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||
unawaited(_persistDiscoveredContacts());
|
||||
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();
|
||||
}
|
||||
|
||||
void deleteAllPaths() {
|
||||
_pathHistoryService?.clearAllHistories();
|
||||
}
|
||||
}
|
||||
|
||||
const int _phRouteMask = 0x03;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
// Buffer Reader - sequential binary data reader with pointer tracking
|
||||
class BufferReader {
|
||||
int _pointer = 0;
|
||||
|
|
@ -37,16 +39,6 @@ class BufferReader {
|
|||
|
||||
Uint8List readRemainingBytes() => readBytes(remaining);
|
||||
|
||||
String readString() {
|
||||
_lastPointer = _pointer;
|
||||
final value = readRemainingBytes();
|
||||
try {
|
||||
return utf8.decode(Uint8List.fromList(value), allowMalformed: true);
|
||||
} catch (e) {
|
||||
return String.fromCharCodes(value); // Latin-1 fallback
|
||||
}
|
||||
}
|
||||
|
||||
String readCStringGreedy(int maxLength) {
|
||||
_lastPointer = _pointer;
|
||||
final value = <int>[];
|
||||
|
|
@ -62,11 +54,12 @@ class BufferReader {
|
|||
}
|
||||
}
|
||||
|
||||
String readCString(int maxLength) {
|
||||
String readCString({int maxLength = -1}) {
|
||||
final backupPointer = _pointer;
|
||||
final value = <int>[];
|
||||
int counter = 0;
|
||||
while (counter < maxLength) {
|
||||
final maxLen = maxLength >= 0 ? maxLength : remaining;
|
||||
while (counter < maxLen) {
|
||||
final byte = readByte();
|
||||
if (byte == 0) break;
|
||||
value.add(byte);
|
||||
|
|
@ -210,16 +203,19 @@ const int cmdSetChannel = 32;
|
|||
const int cmdSendTracePath = 36;
|
||||
const int cmdSetOtherParams = 38;
|
||||
const int cmdSendAnonReq = 57;
|
||||
const int cmdGetTelemetryReq = 39;
|
||||
const int cmdSendTelemetryReq = 39;
|
||||
const int cmdGetCustomVar = 40;
|
||||
const int cmdSetCustomVar = 41;
|
||||
const int cmdSendBinaryReq = 50;
|
||||
const int cmdSetAutoAddConfig = 58;
|
||||
const int cmdGetAutoAddConfig = 59;
|
||||
const int cmdSetPathHashMode = 61;
|
||||
const int cmdGetStats = 56;
|
||||
|
||||
// Text message types
|
||||
const int txtTypePlain = 0;
|
||||
const int txtTypeCliData = 1;
|
||||
const int txtTypeSigned = 2;
|
||||
|
||||
// Repeater request types (for server requests)
|
||||
const int reqTypeGetStatus = 0x01;
|
||||
|
|
@ -251,6 +247,11 @@ const int respCodeChannelMsgRecvV3 = 17;
|
|||
const int respCodeChannelInfo = 18;
|
||||
const int respCodeCustomVars = 21;
|
||||
const int respCodeAutoAddConfig = 25;
|
||||
const int respCodeStats = 24;
|
||||
|
||||
const int statsTypeCore = 0;
|
||||
const int statsTypeRadio = 1;
|
||||
const int statsTypePackets = 2;
|
||||
|
||||
// Push codes (async from device)
|
||||
const int pushCodeAdvert = 0x80;
|
||||
|
|
@ -272,6 +273,10 @@ const int advTypeRepeater = 2;
|
|||
const int advTypeRoom = 3;
|
||||
const int advTypeSensor = 4;
|
||||
|
||||
const int teleModeDeny = 0;
|
||||
const int teleModeAllowFlags = 1; // use contact.flags
|
||||
const int teleModeAllowAll = 2;
|
||||
|
||||
// Payload Types
|
||||
const int payloadTypeREQ =
|
||||
0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
|
||||
|
|
@ -310,6 +315,7 @@ const int autoAddSensorFlag =
|
|||
|
||||
// Sizes
|
||||
const int pubKeySize = 32;
|
||||
const int signatureSize = 64;
|
||||
const int maxPathSize = 64;
|
||||
const int pathHashSize = 1;
|
||||
const int maxNameSize = 32;
|
||||
|
|
@ -352,6 +358,9 @@ const int contactPubKeyOffset = 1;
|
|||
const int contactTypeOffset = 33;
|
||||
const int contactFlagsOffset = 34;
|
||||
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 contactPathOffset = 36;
|
||||
const int contactNameOffset = 100;
|
||||
|
|
@ -370,52 +379,44 @@ const int msgTextOffset = 38;
|
|||
class ParsedContactText {
|
||||
final Uint8List senderPrefix;
|
||||
final String text;
|
||||
|
||||
const ParsedContactText({required this.senderPrefix, required this.text});
|
||||
}
|
||||
|
||||
ParsedContactText? parseContactMessageText(Uint8List frame) {
|
||||
if (frame.isEmpty) return null;
|
||||
final code = frame[0];
|
||||
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
|
||||
|
||||
final message = BufferReader(frame);
|
||||
try {
|
||||
final code = message.readByte();
|
||||
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Companion radio layout:
|
||||
// [code][snr?][res?][res?][prefix x6][path_len][txt_type][timestamp x4][extra?][text...]
|
||||
if (code == respCodeContactMsgRecvV3) {
|
||||
// Skip SNR and reserved bytes in v3 layout
|
||||
message.skipBytes(3);
|
||||
}
|
||||
final senderPrefix = message.readBytes(6); // public key
|
||||
message.skipBytes(1); // path length
|
||||
final textType = message.readByte();
|
||||
message.skipBytes(4); // timestamp (4 bytes)
|
||||
|
||||
final shiftedType = textType >> 2;
|
||||
final isSigned = shiftedType == txtTypeSigned || textType == txtTypeSigned;
|
||||
if (isSigned) {
|
||||
// Signed messages have a 4-byte signature after the timestamp, before the text
|
||||
message.skipBytes(4);
|
||||
}
|
||||
final text = message.readCString();
|
||||
if (text.isEmpty) return null;
|
||||
|
||||
return ParsedContactText(senderPrefix: senderPrefix, text: text);
|
||||
} catch (e) {
|
||||
debugPrint('Error parsing contact message text: $e');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Companion radio layout:
|
||||
// [code][snr?][res?][res?][prefix x6][path_len][txt_type][timestamp x4][extra?][text...]
|
||||
final isV3 = code == respCodeContactMsgRecvV3;
|
||||
final prefixOffset = isV3 ? 4 : 1;
|
||||
const prefixLen = 6;
|
||||
final txtTypeOffset = prefixOffset + prefixLen + 1;
|
||||
final timestampOffset = txtTypeOffset + 1;
|
||||
final baseTextOffset = timestampOffset + 4;
|
||||
if (frame.length <= baseTextOffset) return null;
|
||||
|
||||
final flags = frame[txtTypeOffset];
|
||||
final shiftedType = flags >> 2;
|
||||
final rawType = flags;
|
||||
final isPlain = shiftedType == txtTypePlain || rawType == txtTypePlain;
|
||||
final isCli = shiftedType == txtTypeCliData || rawType == txtTypeCliData;
|
||||
if (!isPlain && !isCli) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var text = readCString(
|
||||
frame,
|
||||
baseTextOffset,
|
||||
frame.length - baseTextOffset,
|
||||
).trim();
|
||||
if (text.isEmpty && frame.length > baseTextOffset + 4) {
|
||||
text = readCString(
|
||||
frame,
|
||||
baseTextOffset + 4,
|
||||
frame.length - (baseTextOffset + 4),
|
||||
).trim();
|
||||
}
|
||||
if (text.isEmpty) return null;
|
||||
|
||||
final senderPrefix = frame.sublist(prefixOffset, prefixOffset + prefixLen);
|
||||
return ParsedContactText(senderPrefix: senderPrefix, text: text);
|
||||
}
|
||||
|
||||
// Helper to read uint32 little-endian
|
||||
|
|
@ -438,18 +439,9 @@ int readInt32LE(Uint8List data, int offset) {
|
|||
return val;
|
||||
}
|
||||
|
||||
// Helper to read null-terminated UTF-8 string
|
||||
String readCString(Uint8List data, int offset, int maxLen) {
|
||||
int end = offset;
|
||||
while (end < offset + maxLen && end < data.length && data[end] != 0) {
|
||||
end++;
|
||||
}
|
||||
try {
|
||||
return utf8.decode(data.sublist(offset, end), allowMalformed: true);
|
||||
} catch (e) {
|
||||
// Fallback to Latin-1 if UTF-8 decoding fails
|
||||
return String.fromCharCodes(data.sublist(offset, end));
|
||||
}
|
||||
// Helper to convert uint32 to hex string
|
||||
String ackHashToHex(int ackHash) {
|
||||
return ackHash.toRadixString(16).padLeft(8, '0');
|
||||
}
|
||||
|
||||
// Helper to convert public key to hex string
|
||||
|
|
@ -569,6 +561,17 @@ Uint8List buildGetBattAndStorageFrame() {
|
|||
return Uint8List.fromList([cmdGetBattAndStorage]);
|
||||
}
|
||||
|
||||
/// Companion radio stats: [56][statsType] where statsType is statsTypeCore/Radio/Packets.
|
||||
Uint8List buildGetStatsFrame(int statsType) {
|
||||
return Uint8List.fromList([cmdGetStats, statsType & 0xFF]);
|
||||
}
|
||||
|
||||
/// Path hash width on air: [61][0][mode], mode 0..2 → (mode+1) bytes per hop hash.
|
||||
Uint8List buildSetPathHashModeFrame(int mode) {
|
||||
final m = mode.clamp(0, 2);
|
||||
return Uint8List.fromList([cmdSetPathHashMode, 0, m]);
|
||||
}
|
||||
|
||||
// Build CMD_SET_DEVICE_TIME frame
|
||||
Uint8List buildSetDeviceTimeFrame(int timestamp) {
|
||||
final writer = BufferWriter();
|
||||
|
|
@ -937,3 +940,18 @@ Uint8List buildSetAutoAddConfigFrame({
|
|||
writer.writeByte(flags);
|
||||
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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,14 @@ import '../l10n/l10n.dart';
|
|||
import '../utils/platform_info.dart';
|
||||
|
||||
class LinkHandler {
|
||||
static TextStyle defaultLinkStyle(BuildContext context, TextStyle base) {
|
||||
final brightness = Theme.of(context).brightness;
|
||||
final orange = brightness == Brightness.dark
|
||||
? const Color(0xFFFFB74D)
|
||||
: const Color(0xFFE65100);
|
||||
return base.copyWith(color: orange, decoration: TextDecoration.underline);
|
||||
}
|
||||
|
||||
/// Returns a [SelectableLinkify] on desktop or a [Linkify] on mobile.
|
||||
static Widget buildLinkifyText({
|
||||
required BuildContext context,
|
||||
|
|
@ -12,14 +20,9 @@ class LinkHandler {
|
|||
required TextStyle style,
|
||||
TextStyle? linkStyle,
|
||||
}) {
|
||||
final effectiveLinkStyle =
|
||||
linkStyle ??
|
||||
style.copyWith(
|
||||
color: Colors.green,
|
||||
decoration: TextDecoration.underline,
|
||||
);
|
||||
final effectiveLinkStyle = linkStyle ?? defaultLinkStyle(context, style);
|
||||
const options = LinkifyOptions(humanize: false, defaultToHttps: false);
|
||||
const linkifiers = [UrlLinkifier()];
|
||||
const linkifiers = [UrlLinkifier(), EmailLinkifier()];
|
||||
void onOpen(LinkableElement link) => handleLinkTap(context, link.url);
|
||||
|
||||
if (PlatformInfo.isDesktop) {
|
||||
|
|
|
|||
|
|
@ -1900,6 +1900,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeight": "Първоначална тежест на маршрута",
|
||||
"appSettings_maxRouteWeight": "Максимално допустимо тегло на маршрута",
|
||||
"appSettings_initialRouteWeightSubtitle": "Начално тегло за новооткрити маршрути",
|
||||
|
|
@ -1914,6 +1943,27 @@
|
|||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"settings_multiAck": "Мулти-потвърди: {value}",
|
||||
"settings_telemetryModeUpdated": "Режим на телеметрията е обновен",
|
||||
"map_showOverlaps": "Покриване на ключа на повтаряча",
|
||||
"map_runTraceWithReturnPath": "Върни се по същия път.",
|
||||
"@radioStats_noiseFloor": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastRssi": {
|
||||
"placeholders": {
|
||||
"rssiDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastSnr": {
|
||||
"placeholders": {
|
||||
"snr": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
|
|
@ -1922,4 +1972,46 @@
|
|||
"scanner_linuxPairingShowPin": "Покажи PIN",
|
||||
"scanner_linuxPairingHidePin": "Скриване на PIN кода",
|
||||
"scanner_linuxPairingPinPrompt": "Въведете PIN кода за {deviceName} (оставете празно, ако няма такъв)."
|
||||
"@radioStats_txAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_rxAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_stripNoise": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_sendCooldown": "Моля, изчакайте малко, преди да изпратите отново.",
|
||||
"appSettings_languageHu": "Унгарски",
|
||||
"appSettings_jumpToOldestUnread": "Преминете към най-старата непочетена статия",
|
||||
"appSettings_jumpToOldestUnreadSubtitle": "Когато отворите чат с непрочетени съобщения, плъзнете надолу, за да видите първото непрочетено съобщение, вместо най-новото.",
|
||||
"appSettings_languageJa": "Японски",
|
||||
"appSettings_languageKo": "Корейски",
|
||||
"radioStats_tooltip": "Статистика за радио и мрежа",
|
||||
"radioStats_screenTitle": "Статистически данни за радиопредаванията",
|
||||
"radioStats_notConnected": "Свържете се с устройство, за да видите статистически данни за радиопредаване.",
|
||||
"radioStats_firmwareTooOld": "Статистиката на радиостанцията изисква съвместимо софтуерно решение версия 8 или по-нова.",
|
||||
"radioStats_waiting": "Изчакване на данни…",
|
||||
"radioStats_noiseFloor": "Ниво на шума: {noiseDbm} dBm",
|
||||
"radioStats_lastRssi": "Последен RSSI: {rssiDbm} dBm",
|
||||
"radioStats_lastSnr": "Последна стойност на SNR: {snr} dB",
|
||||
"radioStats_txAir": "Време на въздух (общо): {seconds} секунди",
|
||||
"radioStats_rxAir": "Общо време на използване на RX (в секунди): {seconds} с",
|
||||
"radioStats_chartCaption": "Ниво на шума (dBm) за последните измервания.",
|
||||
"radioStats_stripNoise": "Ниво на шума: {noiseDbm} dBm",
|
||||
"radioStats_stripWaiting": "Извличане на данни за радиото…",
|
||||
"radioStats_settingsTile": "Статистически данни за радиостанции",
|
||||
"radioStats_settingsSubtitle": "Ниво на шума, RSSI, SNR и време на пренос"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1928,6 +1928,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeightSubtitle": "Ausgangsgewicht für neu entdeckte Pfade",
|
||||
"appSettings_maxRouteWeightSubtitle": "Maximales Gewicht, das ein Weg durch erfolgreiche Lieferungen erreichen kann.",
|
||||
"appSettings_maxRouteWeight": "Maximale Gesamtstreckenlänge",
|
||||
|
|
@ -1942,6 +1971,27 @@
|
|||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"settings_telemetryModeUpdated": "Telemetriemodus aktualisiert",
|
||||
"settings_multiAck": "Mehrfach-Bestätigungen: {value}",
|
||||
"map_showOverlaps": "Überlappungen der Repeater-Taste",
|
||||
"map_runTraceWithReturnPath": "Auf dem gleichen Pfad zurückkehren.",
|
||||
"@radioStats_noiseFloor": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastRssi": {
|
||||
"placeholders": {
|
||||
"rssiDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastSnr": {
|
||||
"placeholders": {
|
||||
"snr": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
|
|
@ -1950,4 +2000,46 @@
|
|||
"scanner_linuxPairingPinPrompt": "Geben Sie den PIN-Code für {deviceName} ein (lassen Sie das Feld leer, falls kein PIN-Code vorhanden ist).",
|
||||
"scanner_linuxPairingShowPin": "PIN anzeigen",
|
||||
"scanner_linuxPairingPinTitle": "PIN für die Bluetooth-Verbindung"
|
||||
"@radioStats_txAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_rxAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_stripNoise": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_sendCooldown": "Bitte warten Sie einen Moment, bevor Sie erneut senden.",
|
||||
"appSettings_jumpToOldestUnread": "Zum ältesten, nicht gelesenen Eintrag springen",
|
||||
"appSettings_languageHu": "Ungarisch",
|
||||
"appSettings_jumpToOldestUnreadSubtitle": "Wenn Sie ein Chatfenster öffnen, in dem Nachrichten vorhanden sind, die noch nicht gelesen wurden, scrollen Sie zu der ersten unlesenen Nachricht, anstatt zur neuesten.",
|
||||
"appSettings_languageJa": "Japanisch",
|
||||
"appSettings_languageKo": "Koreanisch",
|
||||
"radioStats_tooltip": "Daten zu Radio- und Mesh-Netzwerken",
|
||||
"radioStats_screenTitle": "Senderinformationen",
|
||||
"radioStats_notConnected": "Verbinden Sie ein Gerät, um Radiostatisiken anzuzeigen.",
|
||||
"radioStats_firmwareTooOld": "Für die Verwendung der Funkstatistiken ist die Firmware-Version 8 oder höher erforderlich.",
|
||||
"radioStats_waiting": "Warte auf Daten…",
|
||||
"radioStats_noiseFloor": "Rauschpegel: {noiseDbm} dBm",
|
||||
"radioStats_lastRssi": "Letzter RSSI-Wert: {rssiDbm} dBm",
|
||||
"radioStats_lastSnr": "Letzter SNR: {snr} dB",
|
||||
"radioStats_txAir": "Gesamt-TX-Zeit: {seconds} s",
|
||||
"radioStats_rxAir": "Gesamt-RX-Zeit: {seconds} s",
|
||||
"radioStats_chartCaption": "Rauschpegel (dBm) basierend auf den letzten Messwerten.",
|
||||
"radioStats_stripNoise": "Rauschpegel: {noiseDbm} dBm",
|
||||
"radioStats_stripWaiting": "Abrufen von Radiostatus…",
|
||||
"radioStats_settingsTile": "Senderinformationen",
|
||||
"radioStats_settingsSubtitle": "Rauschpegel, RSSI, Signal-Rausch-Verhältnis (SNR) und Nutzzeit"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,6 +177,26 @@
|
|||
"settings_privacyModeToggle": "Toggle privacy mode to hide your name and location in advertisements.",
|
||||
"settings_privacyModeEnabled": "Privacy mode enabled",
|
||||
"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_sendAdvertisement": "Send Advertisement",
|
||||
"settings_sendAdvertisementSubtitle": "Broadcast presence now",
|
||||
|
|
@ -483,6 +503,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_noChannelsConfigured": "No channels configured",
|
||||
"channels_addPublicChannel": "Add Public Channel",
|
||||
|
|
@ -858,6 +889,7 @@
|
|||
"map_chatNodes": "Chat Nodes",
|
||||
"map_repeaters": "Repeaters",
|
||||
"map_otherNodes": "Other Nodes",
|
||||
"map_showOverlaps": "Repeater Key Overlaps",
|
||||
"map_keyPrefix": "Key Prefix",
|
||||
"map_filterByKeyPrefix": "Filter by key prefix",
|
||||
"map_publicKeyPrefix": "Public key prefix",
|
||||
|
|
@ -871,7 +903,8 @@
|
|||
"map_joinRoom": "Join Room",
|
||||
"map_manageRepeater": "Manage Repeater",
|
||||
"map_tapToAdd": "Tap on nodes to add them to the path.",
|
||||
"map_runTrace": "Run Path Trace",
|
||||
"map_runTrace": "Run path trace",
|
||||
"map_runTraceWithReturnPath": "Return back on the same path.",
|
||||
"map_removeLast": "Remove Last",
|
||||
"map_pathTraceCancelled": "Path trace cancelled.",
|
||||
"mapCache_title": "Offline Map Cache",
|
||||
|
|
@ -1955,5 +1988,68 @@
|
|||
"discoveredContacts_copyContact": "Copy Contact to clipboard",
|
||||
"discoveredContacts_deleteContact": "Delete Discovered Contact",
|
||||
"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?",
|
||||
"chat_sendCooldown": "Please wait a moment before sending again.",
|
||||
"appSettings_jumpToOldestUnread": "Jump to oldest unread",
|
||||
"appSettings_jumpToOldestUnreadSubtitle": "When opening a chat with unread messages, scroll to the first unread instead of the latest.",
|
||||
"appSettings_languageHu": "Hungarian",
|
||||
"appSettings_languageJa": "Japanese",
|
||||
"appSettings_languageKo": "Korean",
|
||||
"radioStats_tooltip": "Radio & mesh stats",
|
||||
"radioStats_screenTitle": "Radio stats",
|
||||
"radioStats_notConnected": "Connect to a device to view radio statistics.",
|
||||
"radioStats_firmwareTooOld": "Radio statistics require companion firmware v8 or newer.",
|
||||
"radioStats_waiting": "Waiting for data…",
|
||||
"radioStats_noiseFloor": "Noise floor: {noiseDbm} dBm",
|
||||
"@radioStats_noiseFloor": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"radioStats_lastRssi": "Last RSSI: {rssiDbm} dBm",
|
||||
"@radioStats_lastRssi": {
|
||||
"placeholders": {
|
||||
"rssiDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"radioStats_lastSnr": "Last SNR: {snr} dB",
|
||||
"@radioStats_lastSnr": {
|
||||
"placeholders": {
|
||||
"snr": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"radioStats_txAir": "TX airtime (total): {seconds} s",
|
||||
"@radioStats_txAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"radioStats_rxAir": "RX airtime (total): {seconds} s",
|
||||
"@radioStats_rxAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"radioStats_chartCaption": "Noise floor (dBm) over recent samples.",
|
||||
"radioStats_stripNoise": "Noise floor: {noiseDbm} dBm",
|
||||
"@radioStats_stripNoise": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"radioStats_stripWaiting": "Fetching radio stats…",
|
||||
"radioStats_settingsTile": "Radio stats",
|
||||
"radioStats_settingsSubtitle": "Noise floor, RSSI, SNR, and airtime"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1928,6 +1928,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeight": "Peso inicial de la ruta",
|
||||
"appSettings_maxRouteWeight": "Peso máximo permitido para la ruta",
|
||||
"appSettings_initialRouteWeightSubtitle": "Peso inicial para rutas recién descubiertas",
|
||||
|
|
@ -1942,6 +1971,27 @@
|
|||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"settings_telemetryModeUpdated": "Modo de telemetría actualizado",
|
||||
"settings_multiAck": "Multi-ACKs: {value}",
|
||||
"map_showOverlaps": "Superposiciones de tecla repetidora",
|
||||
"map_runTraceWithReturnPath": "Volver atrás por el mismo camino.",
|
||||
"@radioStats_noiseFloor": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastRssi": {
|
||||
"placeholders": {
|
||||
"rssiDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastSnr": {
|
||||
"placeholders": {
|
||||
"snr": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
|
|
@ -1950,4 +2000,46 @@
|
|||
"scanner_linuxPairingHidePin": "Ocultar PIN",
|
||||
"scanner_linuxPairingPinTitle": "PIN para emparejar dispositivos Bluetooth",
|
||||
"scanner_linuxPairingShowPin": "Mostrar código PIN"
|
||||
"@radioStats_txAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_rxAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_stripNoise": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_jumpToOldestUnread": "Salta a los mensajes más antiguos sin leer",
|
||||
"chat_sendCooldown": "Por favor, espere un momento antes de reenviar.",
|
||||
"appSettings_languageHu": "Húngaro",
|
||||
"appSettings_jumpToOldestUnreadSubtitle": "Cuando abras una conversación con mensajes sin leer, desplázate hacia el primer mensaje sin leer en lugar del más reciente.",
|
||||
"appSettings_languageJa": "Japonés",
|
||||
"appSettings_languageKo": "Coreano",
|
||||
"radioStats_tooltip": "Estadísticas de radio y malla",
|
||||
"radioStats_screenTitle": "Estadísticas de radio",
|
||||
"radioStats_notConnected": "Conéctese a un dispositivo para visualizar estadísticas de radio.",
|
||||
"radioStats_firmwareTooOld": "Las estadísticas de radio requieren un firmware compatible v8 o posterior.",
|
||||
"radioStats_waiting": "Esperando datos…",
|
||||
"radioStats_noiseFloor": "Nivel de ruido: {noiseDbm} dBm",
|
||||
"radioStats_lastRssi": "Último RSSI: {rssiDbm} dBm",
|
||||
"radioStats_lastSnr": "Último SNR: {snr} dB",
|
||||
"radioStats_txAir": "Tiempo de emisión en Texas (total): {seconds} s",
|
||||
"radioStats_rxAir": "Tiempo de transmisión de RX (total): {seconds} s",
|
||||
"radioStats_chartCaption": "Nivel de ruido (dBm) en muestras recientes.",
|
||||
"radioStats_stripNoise": "Nivel de ruido: {noiseDbm} dBm",
|
||||
"radioStats_stripWaiting": "Obteniendo estadísticas de la radio…",
|
||||
"radioStats_settingsTile": "Estadísticas de radio",
|
||||
"radioStats_settingsSubtitle": "Nivel de ruido, RSSI, SNR y tiempo de transmisión"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1900,6 +1900,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_maxRouteWeightSubtitle": "Poids maximal qu'un itinéraire peut accumuler grâce à des livraisons réussies.",
|
||||
"appSettings_initialRouteWeight": "Poids initial de l'itinéraire",
|
||||
"appSettings_maxRouteWeight": "Poids maximal autorisé pour le trajet",
|
||||
|
|
@ -1914,6 +1943,27 @@
|
|||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"settings_multiAck": "Multi-ACKs : {value}",
|
||||
"settings_telemetryModeUpdated": "Le mode télémétrie a été mis à jour",
|
||||
"map_showOverlaps": "Chevauchement de la touche répétitive",
|
||||
"map_runTraceWithReturnPath": "Revenir sur le même chemin.",
|
||||
"@radioStats_noiseFloor": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastRssi": {
|
||||
"placeholders": {
|
||||
"rssiDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastSnr": {
|
||||
"placeholders": {
|
||||
"snr": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
|
|
@ -1922,4 +1972,46 @@
|
|||
"scanner_linuxPairingPinTitle": "Code PIN pour la connexion Bluetooth",
|
||||
"scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si nécessaire).",
|
||||
"scanner_linuxPairingHidePin": "Masquer le code PIN"
|
||||
"@radioStats_txAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_rxAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_stripNoise": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_sendCooldown": "Veuillez patienter un instant avant de réessayer.",
|
||||
"appSettings_jumpToOldestUnread": "Accéder au message le plus ancien non lu",
|
||||
"appSettings_languageHu": "Hongrois",
|
||||
"appSettings_jumpToOldestUnreadSubtitle": "Lorsque vous ouvrez une conversation contenant des messages non lus, faites défiler la page jusqu'au premier message non lu, plutôt que jusqu'au dernier.",
|
||||
"appSettings_languageJa": "Japonais",
|
||||
"appSettings_languageKo": "Coréen",
|
||||
"radioStats_tooltip": "Statistiques des radios et des réseaux sans fil",
|
||||
"radioStats_screenTitle": "Statistiques de radio",
|
||||
"radioStats_notConnected": "Connectez-vous à un appareil pour visualiser les statistiques de la radio.",
|
||||
"radioStats_firmwareTooOld": "Les statistiques radio nécessitent un firmware compatible v8 ou une version ultérieure.",
|
||||
"radioStats_waiting": "En attente des données…",
|
||||
"radioStats_noiseFloor": "Niveau de bruit : {noiseDbm} dBm",
|
||||
"radioStats_lastRssi": "Dernier RSSI : {rssiDbm} dBm",
|
||||
"radioStats_lastSnr": "Dernier SNR : {snr} dB",
|
||||
"radioStats_txAir": "Temps d'antenne à la télévision du Texas (total) : {seconds} s",
|
||||
"radioStats_rxAir": "Temps d'utilisation de l'appareil RX (total) : {seconds} s",
|
||||
"radioStats_chartCaption": "Niveau de bruit (dBm) sur les échantillons récents.",
|
||||
"radioStats_stripNoise": "Niveau de bruit : {noiseDbm} dBm",
|
||||
"radioStats_stripWaiting": "Récupération des statistiques de la radio…",
|
||||
"radioStats_settingsTile": "Statistiques de radio",
|
||||
"radioStats_settingsSubtitle": "Niveau de bruit, RSSI, rapport signal/bruit (SNR) et temps d'antenne"
|
||||
}
|
||||
|
|
|
|||
2048
lib/l10n/app_hu.arb
Normal file
2048
lib/l10n/app_hu.arb
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1900,6 +1900,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeight": "Peso iniziale del percorso",
|
||||
"appSettings_initialRouteWeightSubtitle": "Peso di partenza per nuovi percorsi",
|
||||
"appSettings_maxRouteWeightSubtitle": "Il peso massimo che un percorso può accumulare grazie a consegne di successo.",
|
||||
|
|
@ -1914,6 +1943,27 @@
|
|||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"settings_telemetryModeUpdated": "Modalità telemetria aggiornata",
|
||||
"settings_multiAck": "Multi-ACKs: {value}",
|
||||
"map_showOverlaps": "Sovrapposizioni della chiave ripetitore",
|
||||
"map_runTraceWithReturnPath": "Tornare indietro sullo stesso percorso",
|
||||
"@radioStats_noiseFloor": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastRssi": {
|
||||
"placeholders": {
|
||||
"rssiDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastSnr": {
|
||||
"placeholders": {
|
||||
"snr": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
|
|
@ -1922,4 +1972,46 @@
|
|||
"scanner_linuxPairingHidePin": "Nascondi il PIN",
|
||||
"scanner_linuxPairingPinPrompt": "Inserire il codice PIN per {deviceName} (lasciare vuoto se non presente).",
|
||||
"scanner_linuxPairingPinTitle": "PIN per l'accoppiamento Bluetooth"
|
||||
"@radioStats_txAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_rxAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_stripNoise": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_jumpToOldestUnreadSubtitle": "Quando si apre una chat con messaggi non letti, scorrete verso l'alto fino al primo messaggio non letto, invece che al più recente.",
|
||||
"chat_sendCooldown": "Si prega di attendere un momento prima di inviare nuovamente.",
|
||||
"appSettings_jumpToOldestUnread": "Vai al messaggio più vecchio non letto",
|
||||
"appSettings_languageHu": "Ungherese",
|
||||
"appSettings_languageJa": "Giapponese",
|
||||
"appSettings_languageKo": "Coreano",
|
||||
"radioStats_tooltip": "Statistiche per radio e reti",
|
||||
"radioStats_screenTitle": "Statistiche radio",
|
||||
"radioStats_notConnected": "Connettiti a un dispositivo per visualizzare le statistiche radio.",
|
||||
"radioStats_firmwareTooOld": "Le statistiche radio richiedono il firmware versione 8 o successiva.",
|
||||
"radioStats_noiseFloor": "Livello di rumore: {noiseDbm} dBm",
|
||||
"radioStats_waiting": "In attesa dei dati…",
|
||||
"radioStats_lastRssi": "Ultimo valore RSSI: {rssiDbm} dBm",
|
||||
"radioStats_lastSnr": "Ultimo SNR: {snr} dB",
|
||||
"radioStats_txAir": "Tempo di trasmissione in diretta (totale): {seconds} s",
|
||||
"radioStats_rxAir": "Tempo di trasmissione RX (totale): {seconds} s",
|
||||
"radioStats_chartCaption": "Livello di rumore (dBm) misurato su campioni recenti.",
|
||||
"radioStats_stripNoise": "Livello di rumore: {noiseDbm} dBm",
|
||||
"radioStats_stripWaiting": "Recupero delle statistiche radio…",
|
||||
"radioStats_settingsTile": "Statistiche radio",
|
||||
"radioStats_settingsSubtitle": "Livello di rumore, RSSI, rapporto segnale/rumore (SNR) e tempo di trasmissione"
|
||||
}
|
||||
|
|
|
|||
2048
lib/l10n/app_ja.arb
Normal file
2048
lib/l10n/app_ja.arb
Normal file
File diff suppressed because it is too large
Load diff
2048
lib/l10n/app_ko.arb
Normal file
2048
lib/l10n/app_ko.arb
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -10,7 +10,10 @@ import 'app_localizations_de.dart';
|
|||
import 'app_localizations_en.dart';
|
||||
import 'app_localizations_es.dart';
|
||||
import 'app_localizations_fr.dart';
|
||||
import 'app_localizations_hu.dart';
|
||||
import 'app_localizations_it.dart';
|
||||
import 'app_localizations_ja.dart';
|
||||
import 'app_localizations_ko.dart';
|
||||
import 'app_localizations_nl.dart';
|
||||
import 'app_localizations_pl.dart';
|
||||
import 'app_localizations_pt.dart';
|
||||
|
|
@ -112,7 +115,10 @@ abstract class AppLocalizations {
|
|||
Locale('en'),
|
||||
Locale('es'),
|
||||
Locale('fr'),
|
||||
Locale('hu'),
|
||||
Locale('it'),
|
||||
Locale('ja'),
|
||||
Locale('ko'),
|
||||
Locale('nl'),
|
||||
Locale('pl'),
|
||||
Locale('pt'),
|
||||
|
|
@ -850,6 +856,84 @@ abstract class AppLocalizations {
|
|||
/// **'Privacy mode disabled'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -1870,6 +1954,72 @@ abstract class AppLocalizations {
|
|||
/// **'~ {days} 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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -2932,6 +3082,12 @@ abstract class AppLocalizations {
|
|||
/// **'Other Nodes'**
|
||||
String get map_otherNodes;
|
||||
|
||||
/// No description provided for @map_showOverlaps.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Repeater Key Overlaps'**
|
||||
String get map_showOverlaps;
|
||||
|
||||
/// No description provided for @map_keyPrefix.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -3013,9 +3169,15 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @map_runTrace.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Run Path Trace'**
|
||||
/// **'Run path trace'**
|
||||
String get map_runTrace;
|
||||
|
||||
/// No description provided for @map_runTraceWithReturnPath.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Return back on the same path.'**
|
||||
String get map_runTraceWithReturnPath;
|
||||
|
||||
/// No description provided for @map_removeLast.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -5884,6 +6046,132 @@ abstract class AppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'Are you sure you want to delete all discovered contacts?'**
|
||||
String get discoveredContacts_deleteContactAllContent;
|
||||
|
||||
/// No description provided for @chat_sendCooldown.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Please wait a moment before sending again.'**
|
||||
String get chat_sendCooldown;
|
||||
|
||||
/// No description provided for @appSettings_jumpToOldestUnread.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Jump to oldest unread'**
|
||||
String get appSettings_jumpToOldestUnread;
|
||||
|
||||
/// No description provided for @appSettings_jumpToOldestUnreadSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'When opening a chat with unread messages, scroll to the first unread instead of the latest.'**
|
||||
String get appSettings_jumpToOldestUnreadSubtitle;
|
||||
|
||||
/// No description provided for @appSettings_languageHu.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Hungarian'**
|
||||
String get appSettings_languageHu;
|
||||
|
||||
/// No description provided for @appSettings_languageJa.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Japanese'**
|
||||
String get appSettings_languageJa;
|
||||
|
||||
/// No description provided for @appSettings_languageKo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Korean'**
|
||||
String get appSettings_languageKo;
|
||||
|
||||
/// No description provided for @radioStats_tooltip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Radio & mesh stats'**
|
||||
String get radioStats_tooltip;
|
||||
|
||||
/// No description provided for @radioStats_screenTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Radio stats'**
|
||||
String get radioStats_screenTitle;
|
||||
|
||||
/// No description provided for @radioStats_notConnected.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Connect to a device to view radio statistics.'**
|
||||
String get radioStats_notConnected;
|
||||
|
||||
/// No description provided for @radioStats_firmwareTooOld.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Radio statistics require companion firmware v8 or newer.'**
|
||||
String get radioStats_firmwareTooOld;
|
||||
|
||||
/// No description provided for @radioStats_waiting.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Waiting for data…'**
|
||||
String get radioStats_waiting;
|
||||
|
||||
/// No description provided for @radioStats_noiseFloor.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Noise floor: {noiseDbm} dBm'**
|
||||
String radioStats_noiseFloor(int noiseDbm);
|
||||
|
||||
/// No description provided for @radioStats_lastRssi.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Last RSSI: {rssiDbm} dBm'**
|
||||
String radioStats_lastRssi(int rssiDbm);
|
||||
|
||||
/// No description provided for @radioStats_lastSnr.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Last SNR: {snr} dB'**
|
||||
String radioStats_lastSnr(String snr);
|
||||
|
||||
/// No description provided for @radioStats_txAir.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'TX airtime (total): {seconds} s'**
|
||||
String radioStats_txAir(int seconds);
|
||||
|
||||
/// No description provided for @radioStats_rxAir.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'RX airtime (total): {seconds} s'**
|
||||
String radioStats_rxAir(int seconds);
|
||||
|
||||
/// No description provided for @radioStats_chartCaption.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Noise floor (dBm) over recent samples.'**
|
||||
String get radioStats_chartCaption;
|
||||
|
||||
/// No description provided for @radioStats_stripNoise.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Noise floor: {noiseDbm} dBm'**
|
||||
String radioStats_stripNoise(int noiseDbm);
|
||||
|
||||
/// No description provided for @radioStats_stripWaiting.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Fetching radio stats…'**
|
||||
String get radioStats_stripWaiting;
|
||||
|
||||
/// No description provided for @radioStats_settingsTile.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Radio stats'**
|
||||
String get radioStats_settingsTile;
|
||||
|
||||
/// No description provided for @radioStats_settingsSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Noise floor, RSSI, SNR, and airtime'**
|
||||
String get radioStats_settingsSubtitle;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
|
@ -5902,7 +6190,10 @@ class _AppLocalizationsDelegate
|
|||
'en',
|
||||
'es',
|
||||
'fr',
|
||||
'hu',
|
||||
'it',
|
||||
'ja',
|
||||
'ko',
|
||||
'nl',
|
||||
'pl',
|
||||
'pt',
|
||||
|
|
@ -5931,8 +6222,14 @@ AppLocalizations lookupAppLocalizations(Locale locale) {
|
|||
return AppLocalizationsEs();
|
||||
case 'fr':
|
||||
return AppLocalizationsFr();
|
||||
case 'hu':
|
||||
return AppLocalizationsHu();
|
||||
case 'it':
|
||||
return AppLocalizationsIt();
|
||||
case 'ja':
|
||||
return AppLocalizationsJa();
|
||||
case 'ko':
|
||||
return AppLocalizationsKo();
|
||||
case 'nl':
|
||||
return AppLocalizationsNl();
|
||||
case 'pl':
|
||||
|
|
|
|||
|
|
@ -412,6 +412,52 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||
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
|
||||
String get settings_actions => 'Действия';
|
||||
|
||||
|
|
@ -1003,6 +1049,42 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||
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
|
||||
String get channels_title => 'Канали';
|
||||
|
||||
|
|
@ -1621,6 +1703,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||
@override
|
||||
String get map_otherNodes => 'Други възли';
|
||||
|
||||
@override
|
||||
String get map_showOverlaps => 'Покриване на ключа на повтаряча';
|
||||
|
||||
@override
|
||||
String get map_keyPrefix => 'Префикс на ключа';
|
||||
|
||||
|
|
@ -1665,6 +1750,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||
@override
|
||||
String get map_runTrace => 'Изпълни Път на Следване';
|
||||
|
||||
@override
|
||||
String get map_runTraceWithReturnPath => 'Върни се по същия път.';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Премахни Последно';
|
||||
|
||||
|
|
@ -3410,4 +3498,87 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Сигурни ли сте, че искате да изтриете всички открити контакти?';
|
||||
|
||||
@override
|
||||
String get chat_sendCooldown =>
|
||||
'Моля, изчакайте малко, преди да изпратите отново.';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnread =>
|
||||
'Преминете към най-старата непочетена статия';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnreadSubtitle =>
|
||||
'Когато отворите чат с непрочетени съобщения, плъзнете надолу, за да видите първото непрочетено съобщение, вместо най-новото.';
|
||||
|
||||
@override
|
||||
String get appSettings_languageHu => 'Унгарски';
|
||||
|
||||
@override
|
||||
String get appSettings_languageJa => 'Японски';
|
||||
|
||||
@override
|
||||
String get appSettings_languageKo => 'Корейски';
|
||||
|
||||
@override
|
||||
String get radioStats_tooltip => 'Статистика за радио и мрежа';
|
||||
|
||||
@override
|
||||
String get radioStats_screenTitle =>
|
||||
'Статистически данни за радиопредаванията';
|
||||
|
||||
@override
|
||||
String get radioStats_notConnected =>
|
||||
'Свържете се с устройство, за да видите статистически данни за радиопредаване.';
|
||||
|
||||
@override
|
||||
String get radioStats_firmwareTooOld =>
|
||||
'Статистиката на радиостанцията изисква съвместимо софтуерно решение версия 8 или по-нова.';
|
||||
|
||||
@override
|
||||
String get radioStats_waiting => 'Изчакване на данни…';
|
||||
|
||||
@override
|
||||
String radioStats_noiseFloor(int noiseDbm) {
|
||||
return 'Ниво на шума: $noiseDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastRssi(int rssiDbm) {
|
||||
return 'Последен RSSI: $rssiDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastSnr(String snr) {
|
||||
return 'Последна стойност на SNR: $snr dB';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_txAir(int seconds) {
|
||||
return 'Време на въздух (общо): $seconds секунди';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_rxAir(int seconds) {
|
||||
return 'Общо време на използване на RX (в секунди): $seconds с';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_chartCaption =>
|
||||
'Ниво на шума (dBm) за последните измервания.';
|
||||
|
||||
@override
|
||||
String radioStats_stripNoise(int noiseDbm) {
|
||||
return 'Ниво на шума: $noiseDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_stripWaiting => 'Извличане на данни за радиото…';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsTile => 'Статистически данни за радиостанции';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsSubtitle =>
|
||||
'Ниво на шума, RSSI, SNR и време на пренос';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -412,6 +412,50 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get settings_actions => 'Aktionen';
|
||||
|
||||
|
|
@ -1001,6 +1045,41 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
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
|
||||
String get channels_title => 'Kanäle';
|
||||
|
||||
|
|
@ -1621,6 +1700,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get map_otherNodes => 'Andere Knoten';
|
||||
|
||||
@override
|
||||
String get map_showOverlaps => 'Überlappungen der Repeater-Taste';
|
||||
|
||||
@override
|
||||
String get map_keyPrefix => 'Schlüsselpräfix';
|
||||
|
||||
|
|
@ -1665,6 +1747,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get map_runTrace => 'Pfadverlauf ausführen';
|
||||
|
||||
@override
|
||||
String get map_runTraceWithReturnPath =>
|
||||
'Auf dem gleichen Pfad zurückkehren.';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Letztes Entfernen';
|
||||
|
||||
|
|
@ -3422,4 +3508,86 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Sind Sie sicher, dass Sie alle gefundenen Kontakte löschen möchten?';
|
||||
|
||||
@override
|
||||
String get chat_sendCooldown =>
|
||||
'Bitte warten Sie einen Moment, bevor Sie erneut senden.';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnread =>
|
||||
'Zum ältesten, nicht gelesenen Eintrag springen';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnreadSubtitle =>
|
||||
'Wenn Sie ein Chatfenster öffnen, in dem Nachrichten vorhanden sind, die noch nicht gelesen wurden, scrollen Sie zu der ersten unlesenen Nachricht, anstatt zur neuesten.';
|
||||
|
||||
@override
|
||||
String get appSettings_languageHu => 'Ungarisch';
|
||||
|
||||
@override
|
||||
String get appSettings_languageJa => 'Japanisch';
|
||||
|
||||
@override
|
||||
String get appSettings_languageKo => 'Koreanisch';
|
||||
|
||||
@override
|
||||
String get radioStats_tooltip => 'Daten zu Radio- und Mesh-Netzwerken';
|
||||
|
||||
@override
|
||||
String get radioStats_screenTitle => 'Senderinformationen';
|
||||
|
||||
@override
|
||||
String get radioStats_notConnected =>
|
||||
'Verbinden Sie ein Gerät, um Radiostatisiken anzuzeigen.';
|
||||
|
||||
@override
|
||||
String get radioStats_firmwareTooOld =>
|
||||
'Für die Verwendung der Funkstatistiken ist die Firmware-Version 8 oder höher erforderlich.';
|
||||
|
||||
@override
|
||||
String get radioStats_waiting => 'Warte auf Daten…';
|
||||
|
||||
@override
|
||||
String radioStats_noiseFloor(int noiseDbm) {
|
||||
return 'Rauschpegel: $noiseDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastRssi(int rssiDbm) {
|
||||
return 'Letzter RSSI-Wert: $rssiDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastSnr(String snr) {
|
||||
return 'Letzter SNR: $snr dB';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_txAir(int seconds) {
|
||||
return 'Gesamt-TX-Zeit: $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_rxAir(int seconds) {
|
||||
return 'Gesamt-RX-Zeit: $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_chartCaption =>
|
||||
'Rauschpegel (dBm) basierend auf den letzten Messwerten.';
|
||||
|
||||
@override
|
||||
String radioStats_stripNoise(int noiseDbm) {
|
||||
return 'Rauschpegel: $noiseDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_stripWaiting => 'Abrufen von Radiostatus…';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsTile => 'Senderinformationen';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsSubtitle =>
|
||||
'Rauschpegel, RSSI, Signal-Rausch-Verhältnis (SNR) und Nutzzeit';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -406,6 +406,48 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get settings_actions => 'Actions';
|
||||
|
||||
|
|
@ -986,6 +1028,40 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
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
|
||||
String get channels_title => 'Channels';
|
||||
|
||||
|
|
@ -1594,6 +1670,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get map_otherNodes => 'Other Nodes';
|
||||
|
||||
@override
|
||||
String get map_showOverlaps => 'Repeater Key Overlaps';
|
||||
|
||||
@override
|
||||
String get map_keyPrefix => 'Key Prefix';
|
||||
|
||||
|
|
@ -1634,7 +1713,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get map_tapToAdd => 'Tap on nodes to add them to the path.';
|
||||
|
||||
@override
|
||||
String get map_runTrace => 'Run Path Trace';
|
||||
String get map_runTrace => 'Run path trace';
|
||||
|
||||
@override
|
||||
String get map_runTraceWithReturnPath => 'Return back on the same path.';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Remove Last';
|
||||
|
|
@ -3353,4 +3435,84 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Are you sure you want to delete all discovered contacts?';
|
||||
|
||||
@override
|
||||
String get chat_sendCooldown => 'Please wait a moment before sending again.';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnread => 'Jump to oldest unread';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnreadSubtitle =>
|
||||
'When opening a chat with unread messages, scroll to the first unread instead of the latest.';
|
||||
|
||||
@override
|
||||
String get appSettings_languageHu => 'Hungarian';
|
||||
|
||||
@override
|
||||
String get appSettings_languageJa => 'Japanese';
|
||||
|
||||
@override
|
||||
String get appSettings_languageKo => 'Korean';
|
||||
|
||||
@override
|
||||
String get radioStats_tooltip => 'Radio & mesh stats';
|
||||
|
||||
@override
|
||||
String get radioStats_screenTitle => 'Radio stats';
|
||||
|
||||
@override
|
||||
String get radioStats_notConnected =>
|
||||
'Connect to a device to view radio statistics.';
|
||||
|
||||
@override
|
||||
String get radioStats_firmwareTooOld =>
|
||||
'Radio statistics require companion firmware v8 or newer.';
|
||||
|
||||
@override
|
||||
String get radioStats_waiting => 'Waiting for data…';
|
||||
|
||||
@override
|
||||
String radioStats_noiseFloor(int noiseDbm) {
|
||||
return 'Noise floor: $noiseDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastRssi(int rssiDbm) {
|
||||
return 'Last RSSI: $rssiDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastSnr(String snr) {
|
||||
return 'Last SNR: $snr dB';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_txAir(int seconds) {
|
||||
return 'TX airtime (total): $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_rxAir(int seconds) {
|
||||
return 'RX airtime (total): $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_chartCaption =>
|
||||
'Noise floor (dBm) over recent samples.';
|
||||
|
||||
@override
|
||||
String radioStats_stripNoise(int noiseDbm) {
|
||||
return 'Noise floor: $noiseDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_stripWaiting => 'Fetching radio stats…';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsTile => 'Radio stats';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsSubtitle =>
|
||||
'Noise floor, RSSI, SNR, and airtime';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -411,6 +411,51 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get settings_actions => 'Acciones';
|
||||
|
||||
|
|
@ -1002,6 +1047,42 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||
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
|
||||
String get channels_title => 'Canales';
|
||||
|
||||
|
|
@ -1619,6 +1700,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||
@override
|
||||
String get map_otherNodes => 'Otros Nodos';
|
||||
|
||||
@override
|
||||
String get map_showOverlaps => 'Superposiciones de tecla repetidora';
|
||||
|
||||
@override
|
||||
String get map_keyPrefix => 'Prefijo de clave';
|
||||
|
||||
|
|
@ -1662,6 +1746,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||
@override
|
||||
String get map_runTrace => 'Ejecutar Rastreo de Ruta';
|
||||
|
||||
@override
|
||||
String get map_runTraceWithReturnPath => 'Volver atrás por el mismo camino.';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Eliminar último';
|
||||
|
||||
|
|
@ -3415,4 +3502,86 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'¿Está seguro de que desea eliminar todos los contactos descubiertos!';
|
||||
|
||||
@override
|
||||
String get chat_sendCooldown =>
|
||||
'Por favor, espere un momento antes de reenviar.';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnread =>
|
||||
'Salta a los mensajes más antiguos sin leer';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnreadSubtitle =>
|
||||
'Cuando abras una conversación con mensajes sin leer, desplázate hacia el primer mensaje sin leer en lugar del más reciente.';
|
||||
|
||||
@override
|
||||
String get appSettings_languageHu => 'Húngaro';
|
||||
|
||||
@override
|
||||
String get appSettings_languageJa => 'Japonés';
|
||||
|
||||
@override
|
||||
String get appSettings_languageKo => 'Coreano';
|
||||
|
||||
@override
|
||||
String get radioStats_tooltip => 'Estadísticas de radio y malla';
|
||||
|
||||
@override
|
||||
String get radioStats_screenTitle => 'Estadísticas de radio';
|
||||
|
||||
@override
|
||||
String get radioStats_notConnected =>
|
||||
'Conéctese a un dispositivo para visualizar estadísticas de radio.';
|
||||
|
||||
@override
|
||||
String get radioStats_firmwareTooOld =>
|
||||
'Las estadísticas de radio requieren un firmware compatible v8 o posterior.';
|
||||
|
||||
@override
|
||||
String get radioStats_waiting => 'Esperando datos…';
|
||||
|
||||
@override
|
||||
String radioStats_noiseFloor(int noiseDbm) {
|
||||
return 'Nivel de ruido: $noiseDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastRssi(int rssiDbm) {
|
||||
return 'Último RSSI: $rssiDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastSnr(String snr) {
|
||||
return 'Último SNR: $snr dB';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_txAir(int seconds) {
|
||||
return 'Tiempo de emisión en Texas (total): $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_rxAir(int seconds) {
|
||||
return 'Tiempo de transmisión de RX (total): $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_chartCaption =>
|
||||
'Nivel de ruido (dBm) en muestras recientes.';
|
||||
|
||||
@override
|
||||
String radioStats_stripNoise(int noiseDbm) {
|
||||
return 'Nivel de ruido: $noiseDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_stripWaiting => 'Obteniendo estadísticas de la radio…';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsTile => 'Estadísticas de radio';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsSubtitle =>
|
||||
'Nivel de ruido, RSSI, SNR y tiempo de transmisión';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -415,6 +415,52 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
String get settings_privacyModeDisabled =>
|
||||
'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
|
||||
String get settings_actions => 'Actions';
|
||||
|
||||
|
|
@ -1006,6 +1052,42 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
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
|
||||
String get channels_title => 'Canaux';
|
||||
|
||||
|
|
@ -1628,6 +1710,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
@override
|
||||
String get map_otherNodes => 'Autres nœuds';
|
||||
|
||||
@override
|
||||
String get map_showOverlaps => 'Chevauchement de la touche répétitive';
|
||||
|
||||
@override
|
||||
String get map_keyPrefix => 'Préfixe clé';
|
||||
|
||||
|
|
@ -1672,6 +1757,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
@override
|
||||
String get map_runTrace => 'Exécuter la traçage de chemin';
|
||||
|
||||
@override
|
||||
String get map_runTraceWithReturnPath => 'Revenir sur le même chemin.';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Supprimer le dernier';
|
||||
|
||||
|
|
@ -3438,4 +3526,88 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Êtes-vous sûr de vouloir supprimer tous les contacts découverts ?';
|
||||
|
||||
@override
|
||||
String get chat_sendCooldown =>
|
||||
'Veuillez patienter un instant avant de réessayer.';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnread =>
|
||||
'Accéder au message le plus ancien non lu';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnreadSubtitle =>
|
||||
'Lorsque vous ouvrez une conversation contenant des messages non lus, faites défiler la page jusqu\'au premier message non lu, plutôt que jusqu\'au dernier.';
|
||||
|
||||
@override
|
||||
String get appSettings_languageHu => 'Hongrois';
|
||||
|
||||
@override
|
||||
String get appSettings_languageJa => 'Japonais';
|
||||
|
||||
@override
|
||||
String get appSettings_languageKo => 'Coréen';
|
||||
|
||||
@override
|
||||
String get radioStats_tooltip =>
|
||||
'Statistiques des radios et des réseaux sans fil';
|
||||
|
||||
@override
|
||||
String get radioStats_screenTitle => 'Statistiques de radio';
|
||||
|
||||
@override
|
||||
String get radioStats_notConnected =>
|
||||
'Connectez-vous à un appareil pour visualiser les statistiques de la radio.';
|
||||
|
||||
@override
|
||||
String get radioStats_firmwareTooOld =>
|
||||
'Les statistiques radio nécessitent un firmware compatible v8 ou une version ultérieure.';
|
||||
|
||||
@override
|
||||
String get radioStats_waiting => 'En attente des données…';
|
||||
|
||||
@override
|
||||
String radioStats_noiseFloor(int noiseDbm) {
|
||||
return 'Niveau de bruit : $noiseDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastRssi(int rssiDbm) {
|
||||
return 'Dernier RSSI : $rssiDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastSnr(String snr) {
|
||||
return 'Dernier SNR : $snr dB';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_txAir(int seconds) {
|
||||
return 'Temps d\'antenne à la télévision du Texas (total) : $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_rxAir(int seconds) {
|
||||
return 'Temps d\'utilisation de l\'appareil RX (total) : $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_chartCaption =>
|
||||
'Niveau de bruit (dBm) sur les échantillons récents.';
|
||||
|
||||
@override
|
||||
String radioStats_stripNoise(int noiseDbm) {
|
||||
return 'Niveau de bruit : $noiseDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_stripWaiting =>
|
||||
'Récupération des statistiques de la radio…';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsTile => 'Statistiques de radio';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsSubtitle =>
|
||||
'Niveau de bruit, RSSI, rapport signal/bruit (SNR) et temps d\'antenne';
|
||||
}
|
||||
|
|
|
|||
3589
lib/l10n/app_localizations_hu.dart
Normal file
3589
lib/l10n/app_localizations_hu.dart
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -413,6 +413,52 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get settings_actions => 'Azioni';
|
||||
|
||||
|
|
@ -1002,6 +1048,42 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||
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
|
||||
String get channels_title => 'Canali';
|
||||
|
||||
|
|
@ -1620,6 +1702,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||
@override
|
||||
String get map_otherNodes => 'Altri Nodi';
|
||||
|
||||
@override
|
||||
String get map_showOverlaps => 'Sovrapposizioni della chiave ripetitore';
|
||||
|
||||
@override
|
||||
String get map_keyPrefix => 'Prefisso Chiave';
|
||||
|
||||
|
|
@ -1662,6 +1747,10 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||
@override
|
||||
String get map_runTrace => 'Esegui Path Trace';
|
||||
|
||||
@override
|
||||
String get map_runTraceWithReturnPath =>
|
||||
'Tornare indietro sullo stesso percorso';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Rimuovi ultimo';
|
||||
|
||||
|
|
@ -3417,4 +3506,86 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Sei sicuro di voler eliminare tutti i contatti scoperti?';
|
||||
|
||||
@override
|
||||
String get chat_sendCooldown =>
|
||||
'Si prega di attendere un momento prima di inviare nuovamente.';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnread =>
|
||||
'Vai al messaggio più vecchio non letto';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnreadSubtitle =>
|
||||
'Quando si apre una chat con messaggi non letti, scorrete verso l\'alto fino al primo messaggio non letto, invece che al più recente.';
|
||||
|
||||
@override
|
||||
String get appSettings_languageHu => 'Ungherese';
|
||||
|
||||
@override
|
||||
String get appSettings_languageJa => 'Giapponese';
|
||||
|
||||
@override
|
||||
String get appSettings_languageKo => 'Coreano';
|
||||
|
||||
@override
|
||||
String get radioStats_tooltip => 'Statistiche per radio e reti';
|
||||
|
||||
@override
|
||||
String get radioStats_screenTitle => 'Statistiche radio';
|
||||
|
||||
@override
|
||||
String get radioStats_notConnected =>
|
||||
'Connettiti a un dispositivo per visualizzare le statistiche radio.';
|
||||
|
||||
@override
|
||||
String get radioStats_firmwareTooOld =>
|
||||
'Le statistiche radio richiedono il firmware versione 8 o successiva.';
|
||||
|
||||
@override
|
||||
String get radioStats_waiting => 'In attesa dei dati…';
|
||||
|
||||
@override
|
||||
String radioStats_noiseFloor(int noiseDbm) {
|
||||
return 'Livello di rumore: $noiseDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastRssi(int rssiDbm) {
|
||||
return 'Ultimo valore RSSI: $rssiDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastSnr(String snr) {
|
||||
return 'Ultimo SNR: $snr dB';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_txAir(int seconds) {
|
||||
return 'Tempo di trasmissione in diretta (totale): $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_rxAir(int seconds) {
|
||||
return 'Tempo di trasmissione RX (totale): $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_chartCaption =>
|
||||
'Livello di rumore (dBm) misurato su campioni recenti.';
|
||||
|
||||
@override
|
||||
String radioStats_stripNoise(int noiseDbm) {
|
||||
return 'Livello di rumore: $noiseDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_stripWaiting => 'Recupero delle statistiche radio…';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsTile => 'Statistiche radio';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsSubtitle =>
|
||||
'Livello di rumore, RSSI, rapporto segnale/rumore (SNR) e tempo di trasmissione';
|
||||
}
|
||||
|
|
|
|||
3404
lib/l10n/app_localizations_ja.dart
Normal file
3404
lib/l10n/app_localizations_ja.dart
Normal file
File diff suppressed because it is too large
Load diff
3403
lib/l10n/app_localizations_ko.dart
Normal file
3403
lib/l10n/app_localizations_ko.dart
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -409,6 +409,50 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get settings_actions => 'Acties';
|
||||
|
||||
|
|
@ -994,6 +1038,40 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
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
|
||||
String get channels_title => 'Kanaal';
|
||||
|
||||
|
|
@ -1610,6 +1688,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
@override
|
||||
String get map_otherNodes => 'Andere Nodes';
|
||||
|
||||
@override
|
||||
String get map_showOverlaps => 'Herhalingssleutel overlapt';
|
||||
|
||||
@override
|
||||
String get map_keyPrefix => 'Prefix sleutel';
|
||||
|
||||
|
|
@ -1654,6 +1735,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
@override
|
||||
String get map_runTrace => 'Padeshulp traceren';
|
||||
|
||||
@override
|
||||
String get map_runTraceWithReturnPath => 'Terugkeren op hetzelfde pad.';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Verwijder Laatste';
|
||||
|
||||
|
|
@ -3399,4 +3483,86 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Weet u zeker dat u alle ontdekte contacten wilt verwijderen?';
|
||||
|
||||
@override
|
||||
String get chat_sendCooldown =>
|
||||
'Gelieve even te wachten voordat u opnieuw verzendt.';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnread =>
|
||||
'Ga naar het oudste ongelezen bericht';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnreadSubtitle =>
|
||||
'Bij het openen van een chat met ongelezen berichten, scroll dan naar het eerste ongelezen bericht, in plaats van naar het meest recente.';
|
||||
|
||||
@override
|
||||
String get appSettings_languageHu => 'Hongaars';
|
||||
|
||||
@override
|
||||
String get appSettings_languageJa => 'Japanisch';
|
||||
|
||||
@override
|
||||
String get appSettings_languageKo => 'Koreaans';
|
||||
|
||||
@override
|
||||
String get radioStats_tooltip => 'Statistieken voor radio en mesh-netwerken';
|
||||
|
||||
@override
|
||||
String get radioStats_screenTitle => 'Statistieken over radio';
|
||||
|
||||
@override
|
||||
String get radioStats_notConnected =>
|
||||
'Verbind met een apparaat om radio-statistieken te bekijken.';
|
||||
|
||||
@override
|
||||
String get radioStats_firmwareTooOld =>
|
||||
'Om de statistieken via radio te kunnen gebruiken, is firmware versie 8 of een nieuwere vereist.';
|
||||
|
||||
@override
|
||||
String get radioStats_waiting => 'Wacht op gegevens…';
|
||||
|
||||
@override
|
||||
String radioStats_noiseFloor(int noiseDbm) {
|
||||
return 'Ruisfrequentie: $noiseDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastRssi(int rssiDbm) {
|
||||
return 'Laatste RSSI-waarde: $rssiDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastSnr(String snr) {
|
||||
return 'Laatste SNR: $snr dB';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_txAir(int seconds) {
|
||||
return 'TX-tijd (totaal): $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_rxAir(int seconds) {
|
||||
return 'Tijd besteed met RX (totaal): $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_chartCaption =>
|
||||
'Ruisfrequentie (dBm) over recente metingen.';
|
||||
|
||||
@override
|
||||
String radioStats_stripNoise(int noiseDbm) {
|
||||
return 'Ruisfrequentie: $noiseDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_stripWaiting => 'Radio-statistieken ophalen…';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsTile => 'Statistieken over radio';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsSubtitle =>
|
||||
'Ruimtelijke ruis, RSSI, SNR en beschikbare tijd';
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -412,6 +412,51 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get settings_actions => 'Ações';
|
||||
|
||||
|
|
@ -1002,6 +1047,42 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||
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
|
||||
String get channels_title => 'Canais';
|
||||
|
||||
|
|
@ -1619,6 +1700,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||
@override
|
||||
String get map_otherNodes => 'Outros Nós';
|
||||
|
||||
@override
|
||||
String get map_showOverlaps => 'Sobreposições da Chave Repeater';
|
||||
|
||||
@override
|
||||
String get map_keyPrefix => 'Prefixo Chave';
|
||||
|
||||
|
|
@ -1662,6 +1746,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||
@override
|
||||
String get map_runTrace => 'Executar Traçado de Caminho';
|
||||
|
||||
@override
|
||||
String get map_runTraceWithReturnPath => 'Retornar ao mesmo caminho.';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Remover Último';
|
||||
|
||||
|
|
@ -3411,4 +3498,86 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Tem certeza de que deseja excluir todos os contatos descobertos?';
|
||||
|
||||
@override
|
||||
String get chat_sendCooldown =>
|
||||
'Por favor, aguarde um momento antes de reenviar.';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnread =>
|
||||
'Vá para a mensagem mais antiga não lida';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnreadSubtitle =>
|
||||
'Ao abrir uma conversa com mensagens não lidas, role para a primeira mensagem não lida, em vez da mais recente.';
|
||||
|
||||
@override
|
||||
String get appSettings_languageHu => 'Húngaro';
|
||||
|
||||
@override
|
||||
String get appSettings_languageJa => 'Japonês';
|
||||
|
||||
@override
|
||||
String get appSettings_languageKo => 'Coreano';
|
||||
|
||||
@override
|
||||
String get radioStats_tooltip => 'Estatísticas de rádio e malha';
|
||||
|
||||
@override
|
||||
String get radioStats_screenTitle => 'Estatísticas de rádio';
|
||||
|
||||
@override
|
||||
String get radioStats_notConnected =>
|
||||
'Conecte-se a um dispositivo para visualizar estatísticas de rádio.';
|
||||
|
||||
@override
|
||||
String get radioStats_firmwareTooOld =>
|
||||
'As estatísticas de rádio exigem o firmware v8 ou uma versão mais recente.';
|
||||
|
||||
@override
|
||||
String get radioStats_waiting => 'Aguardando dados…';
|
||||
|
||||
@override
|
||||
String radioStats_noiseFloor(int noiseDbm) {
|
||||
return 'Nível de ruído: $noiseDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastRssi(int rssiDbm) {
|
||||
return 'Último RSSI: $rssiDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastSnr(String snr) {
|
||||
return 'Último SNR: $snr dB';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_txAir(int seconds) {
|
||||
return 'Tempo de transmissão da TX (total): $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_rxAir(int seconds) {
|
||||
return 'Tempo de uso do RX (total): $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_chartCaption =>
|
||||
'Nível de ruído (dBm) em amostras recentes.';
|
||||
|
||||
@override
|
||||
String radioStats_stripNoise(int noiseDbm) {
|
||||
return 'Nível de ruído: $noiseDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_stripWaiting => 'Obtendo estatísticas de rádio…';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsTile => 'Estatísticas de rádio';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsSubtitle =>
|
||||
'Nível de ruído, RSSI, SNR e tempo de transmissão';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -413,6 +413,51 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
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
|
||||
String get settings_actions => 'Действия';
|
||||
|
||||
|
|
@ -1003,6 +1048,42 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
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
|
||||
String get channels_title => 'Каналы';
|
||||
|
||||
|
|
@ -1623,6 +1704,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
@override
|
||||
String get map_otherNodes => 'Другие ноды';
|
||||
|
||||
@override
|
||||
String get map_showOverlaps => 'Перекрытия ключа повтора';
|
||||
|
||||
@override
|
||||
String get map_keyPrefix => 'Префикс ключа';
|
||||
|
||||
|
|
@ -1666,6 +1750,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
@override
|
||||
String get map_runTrace => 'Запустить трассировку пути';
|
||||
|
||||
@override
|
||||
String get map_runTraceWithReturnPath => 'Вернуться обратно по тому же пути';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Удалить последний';
|
||||
|
||||
|
|
@ -3426,4 +3513,86 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Вы уверены, что хотите удалить все обнаруженные контакты?';
|
||||
|
||||
@override
|
||||
String get chat_sendCooldown =>
|
||||
'Пожалуйста, подождите немного, прежде чем отправлять сообщение снова.';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnread =>
|
||||
'Перейти к самому старому непрочитанному сообщению';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnreadSubtitle =>
|
||||
'При открытии чата с непрочитанными сообщениями, прокрутите страницу, чтобы увидеть первое непрочитанное сообщение, а не последнее.';
|
||||
|
||||
@override
|
||||
String get appSettings_languageHu => 'Венгерский';
|
||||
|
||||
@override
|
||||
String get appSettings_languageJa => 'Японский';
|
||||
|
||||
@override
|
||||
String get appSettings_languageKo => 'Корейский';
|
||||
|
||||
@override
|
||||
String get radioStats_tooltip => 'Статистика радио и беспроводной сети';
|
||||
|
||||
@override
|
||||
String get radioStats_screenTitle => 'Статистика радиовещания';
|
||||
|
||||
@override
|
||||
String get radioStats_notConnected =>
|
||||
'Подключитесь к устройству, чтобы просмотреть статистику радио.';
|
||||
|
||||
@override
|
||||
String get radioStats_firmwareTooOld =>
|
||||
'Для работы радиостатистики требуется установленная версия прошивки v8 или более новая.';
|
||||
|
||||
@override
|
||||
String get radioStats_waiting => 'Ожидаем данных…';
|
||||
|
||||
@override
|
||||
String radioStats_noiseFloor(int noiseDbm) {
|
||||
return 'Уровень шума: $noiseDbm дБм';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastRssi(int rssiDbm) {
|
||||
return 'Последнее значение RSSI: $rssiDbm дБм';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastSnr(String snr) {
|
||||
return 'Последнее значение SNR: $snr дБ';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_txAir(int seconds) {
|
||||
return 'Время эфира на телеканале TX (общее): $seconds секунд';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_rxAir(int seconds) {
|
||||
return 'Общее время использования RX (в секундах): $seconds с';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_chartCaption =>
|
||||
'Уровень шума (дБм) на основе последних измерений.';
|
||||
|
||||
@override
|
||||
String radioStats_stripNoise(int noiseDbm) {
|
||||
return 'Уровень шума: $noiseDbm дБм';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_stripWaiting => 'Получение данных о радио…';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsTile => 'Статистика радиовещания';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsSubtitle =>
|
||||
'Уровень шума, RSSI, SNR и время передачи';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -409,6 +409,49 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get settings_actions => 'Možné akcie';
|
||||
|
||||
|
|
@ -994,6 +1037,41 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||
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
|
||||
String get channels_title => 'Kanály';
|
||||
|
||||
|
|
@ -1611,6 +1689,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||
@override
|
||||
String get map_otherNodes => 'Ostatné uzly';
|
||||
|
||||
@override
|
||||
String get map_showOverlaps => 'Prekrývanie opakovača kľúča';
|
||||
|
||||
@override
|
||||
String get map_keyPrefix => 'Päťciferné predpona';
|
||||
|
||||
|
|
@ -1654,6 +1735,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||
@override
|
||||
String get map_runTrace => 'Spustiť trasovaním cesty';
|
||||
|
||||
@override
|
||||
String get map_runTraceWithReturnPath => 'Vráťte sa späť po tej istej ceste.';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Odstrániť posledný';
|
||||
|
||||
|
|
@ -3394,4 +3478,84 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Ste si istí, že chcete zmazať všetky objavené kontakty?';
|
||||
|
||||
@override
|
||||
String get chat_sendCooldown => 'Prosím, počkajte chvíľu, než zašlete znova.';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnread => 'Presk oceň';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnreadSubtitle =>
|
||||
'Pri otvorení chatu s neprečítanými správami, prejdite do prvého neprečítaného, namiesto poslednej.';
|
||||
|
||||
@override
|
||||
String get appSettings_languageHu => 'Maďarský';
|
||||
|
||||
@override
|
||||
String get appSettings_languageJa => 'Japonský';
|
||||
|
||||
@override
|
||||
String get appSettings_languageKo => 'Kórejský';
|
||||
|
||||
@override
|
||||
String get radioStats_tooltip => 'Statistiky rádiových a sieťových kanálov';
|
||||
|
||||
@override
|
||||
String get radioStats_screenTitle => 'Štatistiky rádiových vysielaní';
|
||||
|
||||
@override
|
||||
String get radioStats_notConnected =>
|
||||
'Pripojte sa k zariadeniu, aby ste mohli sledovať štatistiky rádiového vysielania.';
|
||||
|
||||
@override
|
||||
String get radioStats_firmwareTooOld =>
|
||||
'Statistické údaje z rádia vyžadujú sprievodný softvér verzie v8 alebo novšej.';
|
||||
|
||||
@override
|
||||
String get radioStats_waiting => 'Čakám na údaje…';
|
||||
|
||||
@override
|
||||
String radioStats_noiseFloor(int noiseDbm) {
|
||||
return 'Úroveň hluku: $noiseDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastRssi(int rssiDbm) {
|
||||
return 'Posledný údaj RSSI: $rssiDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastSnr(String snr) {
|
||||
return 'Posledná hodnota SNR: $snr dB';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_txAir(int seconds) {
|
||||
return 'Čas vysielania na TX (celkový): $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_rxAir(int seconds) {
|
||||
return 'Čas RX (celkový): $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_chartCaption =>
|
||||
'Úroveň šumu (dBm) pre posledné vzorky.';
|
||||
|
||||
@override
|
||||
String radioStats_stripNoise(int noiseDbm) {
|
||||
return 'Úroveň hluku: $noiseDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_stripWaiting => 'Získavanie údajov o rádiu…';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsTile => 'Štatistiky rádiových vysielaní';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsSubtitle =>
|
||||
'Úroveň hluku, RSSI, SNR a časové rozloženie';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -408,6 +408,50 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get settings_actions => 'Akcije';
|
||||
|
||||
|
|
@ -992,6 +1036,41 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||
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
|
||||
String get channels_title => 'Kanali';
|
||||
|
||||
|
|
@ -1607,6 +1686,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||
@override
|
||||
String get map_otherNodes => 'Druge vozlišča';
|
||||
|
||||
@override
|
||||
String get map_showOverlaps => 'Prekrivanje ključa ponovnega predvajanja';
|
||||
|
||||
@override
|
||||
String get map_keyPrefix => 'Predpona ključa';
|
||||
|
||||
|
|
@ -1649,6 +1731,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||
@override
|
||||
String get map_runTrace => 'Zaženi sledenje poti';
|
||||
|
||||
@override
|
||||
String get map_runTraceWithReturnPath => 'Vrni se nazaj po isti poti.';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Odstrani Zadnji';
|
||||
|
||||
|
|
@ -3397,4 +3482,86 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Ste prepričani, da želite izbrisati vse odkrite kontakte?';
|
||||
|
||||
@override
|
||||
String get chat_sendCooldown =>
|
||||
'Prosimo, počakajte trenutek, preden pošljete ponovno.';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnread =>
|
||||
'Pritisnite za najstarejše nepročitano sporočilo';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnreadSubtitle =>
|
||||
'Ko odpirate klepet z neprebranimi sporočili, se premaknite na prvo neprebrano sporočilo, namesto najnovejšega.';
|
||||
|
||||
@override
|
||||
String get appSettings_languageHu => 'Madžarski';
|
||||
|
||||
@override
|
||||
String get appSettings_languageJa => 'Japonski';
|
||||
|
||||
@override
|
||||
String get appSettings_languageKo => 'Korejski';
|
||||
|
||||
@override
|
||||
String get radioStats_tooltip => 'Statistike za radio in mrežo';
|
||||
|
||||
@override
|
||||
String get radioStats_screenTitle => 'Radijske statistike';
|
||||
|
||||
@override
|
||||
String get radioStats_notConnected =>
|
||||
'Povežite se z napravo, da si ogledate statistiko o radiju.';
|
||||
|
||||
@override
|
||||
String get radioStats_firmwareTooOld =>
|
||||
'Statistika za radio zahteva združljivo programsko opremo v8 ali kasnejše.';
|
||||
|
||||
@override
|
||||
String get radioStats_waiting => 'Čakam na podatke…';
|
||||
|
||||
@override
|
||||
String radioStats_noiseFloor(int noiseDbm) {
|
||||
return 'Število šuma: $noiseDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastRssi(int rssiDbm) {
|
||||
return 'Najkasnejše vrednost RSSI: $rssiDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastSnr(String snr) {
|
||||
return 'Najkasnejše vrednost SNR: $snr dB';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_txAir(int seconds) {
|
||||
return 'Čas na TX (skupno): $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_rxAir(int seconds) {
|
||||
return 'Čas, namenjen RX-ju (skupno): $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_chartCaption =>
|
||||
'Ravnovredna raven šuma (dBm) za nedavne vzorce.';
|
||||
|
||||
@override
|
||||
String radioStats_stripNoise(int noiseDbm) {
|
||||
return 'Število šuma: $noiseDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_stripWaiting => 'Prejemanje statistike o radiju…';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsTile => 'Radijske statistike';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsSubtitle =>
|
||||
'Število šumov, RSSI, SNR in čas, ki ga je napolnila oprema';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -406,6 +406,49 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get settings_actions => 'Åtgärder';
|
||||
|
||||
|
|
@ -986,6 +1029,40 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||
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
|
||||
String get channels_title => 'Kanaler';
|
||||
|
||||
|
|
@ -1601,6 +1678,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||
@override
|
||||
String get map_otherNodes => 'Andra noder';
|
||||
|
||||
@override
|
||||
String get map_showOverlaps => 'Repeater-nyckelöverlappningar';
|
||||
|
||||
@override
|
||||
String get map_keyPrefix => 'Nyckelprefix';
|
||||
|
||||
|
|
@ -1644,6 +1724,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||
@override
|
||||
String get map_runTrace => 'Kör spårsökning';
|
||||
|
||||
@override
|
||||
String get map_runTraceWithReturnPath => 'Gå tillbaka på samma väg';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Ta bort sista';
|
||||
|
||||
|
|
@ -3375,4 +3458,86 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Är du säker på att du vill ta bort alla upptäckta kontakter?';
|
||||
|
||||
@override
|
||||
String get chat_sendCooldown =>
|
||||
'Vänligen vänta en stund innan du skickar igen.';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnread =>
|
||||
'Gå direkt till det äldsta, obesvarade meddelandet';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnreadSubtitle =>
|
||||
'När du öppnar en chatt med oinlästa meddelanden, scrolla till det första oinlästa meddelandet istället för det senaste.';
|
||||
|
||||
@override
|
||||
String get appSettings_languageHu => 'Ungerskt';
|
||||
|
||||
@override
|
||||
String get appSettings_languageJa => 'Japanska';
|
||||
|
||||
@override
|
||||
String get appSettings_languageKo => 'Koreanska';
|
||||
|
||||
@override
|
||||
String get radioStats_tooltip => 'Radio- och mesh-statistik';
|
||||
|
||||
@override
|
||||
String get radioStats_screenTitle => 'Radiostation';
|
||||
|
||||
@override
|
||||
String get radioStats_notConnected =>
|
||||
'Anslut till en enhet för att visa radiostatistik.';
|
||||
|
||||
@override
|
||||
String get radioStats_firmwareTooOld =>
|
||||
'Radio statistik kräver kompatibel firmware version 8 eller senare.';
|
||||
|
||||
@override
|
||||
String get radioStats_waiting => 'Väntar på data…';
|
||||
|
||||
@override
|
||||
String radioStats_noiseFloor(int noiseDbm) {
|
||||
return 'Bakgrundsnivå: $noiseDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastRssi(int rssiDbm) {
|
||||
return 'Senaste RSSI-värde: $rssiDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastSnr(String snr) {
|
||||
return 'Senaste SNR: $snr dB';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_txAir(int seconds) {
|
||||
return 'TX-tid (total): $seconds sekunder';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_rxAir(int seconds) {
|
||||
return 'RX-tid (total): $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_chartCaption =>
|
||||
'Ljudnivå (dBm) baserat på de senaste mätningarna.';
|
||||
|
||||
@override
|
||||
String radioStats_stripNoise(int noiseDbm) {
|
||||
return 'Bakgrundsnivå: $noiseDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_stripWaiting => 'Hämtar radiostatistik…';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsTile => 'Radiostation';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsSubtitle =>
|
||||
'Bakgrundsnivå, RSSI, SNR och tillgänglig tid';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -410,6 +410,50 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get settings_actions => 'Дії';
|
||||
|
||||
|
|
@ -998,6 +1042,42 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||
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
|
||||
String get channels_title => 'Канали';
|
||||
|
||||
|
|
@ -1619,6 +1699,9 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||
@override
|
||||
String get map_otherNodes => 'Інші вузли';
|
||||
|
||||
@override
|
||||
String get map_showOverlaps => 'Перекриття ключа повторювача';
|
||||
|
||||
@override
|
||||
String get map_keyPrefix => 'Префікс ключа';
|
||||
|
||||
|
|
@ -1662,6 +1745,9 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||
@override
|
||||
String get map_runTrace => 'Виконати трасування шляху';
|
||||
|
||||
@override
|
||||
String get map_runTraceWithReturnPath => 'Повернутися назад тим же шляхом';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Видалити останній';
|
||||
|
||||
|
|
@ -3430,4 +3516,86 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Ви впевнені, що хочете видалити всі виявлені контакти?';
|
||||
|
||||
@override
|
||||
String get chat_sendCooldown =>
|
||||
'Будь ласка, зачекайте трохи, перш ніж відправляти знову.';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnread =>
|
||||
'Перейти до найстарішого непрочитаного повідомлення';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnreadSubtitle =>
|
||||
'При відкритті чату з не прочитаними повідомленнями, прокрутіть до першого не прочитаного повідомлення, а не до останнього.';
|
||||
|
||||
@override
|
||||
String get appSettings_languageHu => 'Угорський';
|
||||
|
||||
@override
|
||||
String get appSettings_languageJa => 'Японська';
|
||||
|
||||
@override
|
||||
String get appSettings_languageKo => 'Кореєська';
|
||||
|
||||
@override
|
||||
String get radioStats_tooltip => 'Статистика радіо та мережі';
|
||||
|
||||
@override
|
||||
String get radioStats_screenTitle => 'Дані про радіостанції';
|
||||
|
||||
@override
|
||||
String get radioStats_notConnected =>
|
||||
'Підключіться до пристрою, щоб переглядати статистику радіопередач.';
|
||||
|
||||
@override
|
||||
String get radioStats_firmwareTooOld =>
|
||||
'Статистика радіо приймача вимагає супутнього програмного забезпечення версії 8 або новішої.';
|
||||
|
||||
@override
|
||||
String get radioStats_waiting => 'Очікую на отримання даних…';
|
||||
|
||||
@override
|
||||
String radioStats_noiseFloor(int noiseDbm) {
|
||||
return 'Рівень шуму: $noiseDbm дБм';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastRssi(int rssiDbm) {
|
||||
return 'Останній показник RSSI: $rssiDbm дБм';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastSnr(String snr) {
|
||||
return 'Останній показник SNR: $snr дБ';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_txAir(int seconds) {
|
||||
return 'Час трансляції на телеканалі TX (загальний): $seconds секунд';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_rxAir(int seconds) {
|
||||
return 'Загальний час використання RX: $seconds секунд';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_chartCaption =>
|
||||
'Рівень шуму (дБм) на основі останніх вимірювань.';
|
||||
|
||||
@override
|
||||
String radioStats_stripNoise(int noiseDbm) {
|
||||
return 'Рівень шуму: $noiseDbm дБм';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_stripWaiting => 'Отримано статистику радіо…';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsTile => 'Дані про радіостанції';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsSubtitle =>
|
||||
'Рівень шуму, RSSI, SNR та час, протягом якого пристрій використовує радіоканал.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -388,6 +388,47 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get settings_actions => '操作';
|
||||
|
||||
|
|
@ -937,6 +978,39 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||
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
|
||||
String get channels_title => '频道';
|
||||
|
||||
|
|
@ -1522,6 +1596,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||
@override
|
||||
String get map_otherNodes => '其他节点';
|
||||
|
||||
@override
|
||||
String get map_showOverlaps => '重复键重叠';
|
||||
|
||||
@override
|
||||
String get map_keyPrefix => '关键字前缀';
|
||||
|
||||
|
|
@ -1564,6 +1641,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||
@override
|
||||
String get map_runTrace => '运行路径追踪';
|
||||
|
||||
@override
|
||||
String get map_runTraceWithReturnPath => '沿着相同的路径返回';
|
||||
|
||||
@override
|
||||
String get map_removeLast => '移除最后一个';
|
||||
|
||||
|
|
@ -3156,4 +3236,80 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAllContent => '您确定要删除所有发现的联系人吗?';
|
||||
|
||||
@override
|
||||
String get chat_sendCooldown => '请稍等片刻后再尝试发送。';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnread => '跳转到最旧、未读的文章';
|
||||
|
||||
@override
|
||||
String get appSettings_jumpToOldestUnreadSubtitle =>
|
||||
'在打开包含未读消息的聊天时,请滚动到第一个未读消息,而不是最新的消息。';
|
||||
|
||||
@override
|
||||
String get appSettings_languageHu => '匈牙利';
|
||||
|
||||
@override
|
||||
String get appSettings_languageJa => '日语';
|
||||
|
||||
@override
|
||||
String get appSettings_languageKo => '韩语';
|
||||
|
||||
@override
|
||||
String get radioStats_tooltip => '无线电和网状结构统计数据';
|
||||
|
||||
@override
|
||||
String get radioStats_screenTitle => '广播统计数据';
|
||||
|
||||
@override
|
||||
String get radioStats_notConnected => '连接到设备以查看收音机统计信息。';
|
||||
|
||||
@override
|
||||
String get radioStats_firmwareTooOld => '使用无线电统计功能需要配合使用 v8 或更高版本的固件。';
|
||||
|
||||
@override
|
||||
String get radioStats_waiting => '正在等待数据…';
|
||||
|
||||
@override
|
||||
String radioStats_noiseFloor(int noiseDbm) {
|
||||
return '噪声水平:$noiseDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastRssi(int rssiDbm) {
|
||||
return '上次 RSSI 值:$rssiDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_lastSnr(String snr) {
|
||||
return '上次 SNR:$snr dB';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_txAir(int seconds) {
|
||||
return 'TX 频道播出时间(总时长):$seconds 秒';
|
||||
}
|
||||
|
||||
@override
|
||||
String radioStats_rxAir(int seconds) {
|
||||
return 'RX 使用时长(总时长):$seconds 秒';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_chartCaption => '近期的噪声水平(dBm)。';
|
||||
|
||||
@override
|
||||
String radioStats_stripNoise(int noiseDbm) {
|
||||
return '噪声水平:$noiseDbm dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String get radioStats_stripWaiting => '正在获取收音机数据…';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsTile => '广播统计数据';
|
||||
|
||||
@override
|
||||
String get radioStats_settingsSubtitle => '噪声水平、RSSI、信噪比和空中时间';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1900,6 +1900,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_maxRouteWeightSubtitle": "Het maximale gewicht dat een route kan bereiken door succesvolle leveringen.",
|
||||
"appSettings_initialRouteWeight": "เริ่มต้น gewicht van de route",
|
||||
"appSettings_maxRouteWeight": "Maximale gewicht voor de route",
|
||||
|
|
@ -1914,6 +1943,27 @@
|
|||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"settings_telemetryModeUpdated": "Telemetrie-modus bijgewerkt",
|
||||
"settings_multiAck": "Multi-ACKs: {value}",
|
||||
"map_showOverlaps": "Herhalingssleutel overlapt",
|
||||
"map_runTraceWithReturnPath": "Terugkeren op hetzelfde pad.",
|
||||
"@radioStats_noiseFloor": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastRssi": {
|
||||
"placeholders": {
|
||||
"rssiDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastSnr": {
|
||||
"placeholders": {
|
||||
"snr": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
|
|
@ -1922,4 +1972,46 @@
|
|||
"scanner_linuxPairingShowPin": "Toon PIN",
|
||||
"scanner_linuxPairingHidePin": "Verberg PIN",
|
||||
"scanner_linuxPairingPinPrompt": "Voer het pincode-in voor {deviceName} in (laat dit leeg als er geen is)."
|
||||
"@radioStats_txAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_rxAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_stripNoise": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_jumpToOldestUnread": "Ga naar het oudste ongelezen bericht",
|
||||
"appSettings_jumpToOldestUnreadSubtitle": "Bij het openen van een chat met ongelezen berichten, scroll dan naar het eerste ongelezen bericht, in plaats van naar het meest recente.",
|
||||
"chat_sendCooldown": "Gelieve even te wachten voordat u opnieuw verzendt.",
|
||||
"appSettings_languageHu": "Hongaars",
|
||||
"appSettings_languageJa": "Japanisch",
|
||||
"appSettings_languageKo": "Koreaans",
|
||||
"radioStats_tooltip": "Statistieken voor radio en mesh-netwerken",
|
||||
"radioStats_screenTitle": "Statistieken over radio",
|
||||
"radioStats_notConnected": "Verbind met een apparaat om radio-statistieken te bekijken.",
|
||||
"radioStats_firmwareTooOld": "Om de statistieken via radio te kunnen gebruiken, is firmware versie 8 of een nieuwere vereist.",
|
||||
"radioStats_waiting": "Wacht op gegevens…",
|
||||
"radioStats_noiseFloor": "Ruisfrequentie: {noiseDbm} dBm",
|
||||
"radioStats_lastRssi": "Laatste RSSI-waarde: {rssiDbm} dBm",
|
||||
"radioStats_lastSnr": "Laatste SNR: {snr} dB",
|
||||
"radioStats_txAir": "TX-tijd (totaal): {seconds} s",
|
||||
"radioStats_rxAir": "Tijd besteed met RX (totaal): {seconds} s",
|
||||
"radioStats_chartCaption": "Ruisfrequentie (dBm) over recente metingen.",
|
||||
"radioStats_stripNoise": "Ruisfrequentie: {noiseDbm} dBm",
|
||||
"radioStats_stripWaiting": "Radio-statistieken ophalen…",
|
||||
"radioStats_settingsTile": "Statistieken over radio",
|
||||
"radioStats_settingsSubtitle": "Ruimtelijke ruis, RSSI, SNR en beschikbare tijd"
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1900,6 +1900,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeight": "Peso Inicial da Rota",
|
||||
"appSettings_maxRouteWeight": "Peso Máximo da Rota",
|
||||
"appSettings_maxRouteWeightSubtitle": "Peso máximo que um determinado percurso pode acumular com entregas bem-sucedidas.",
|
||||
|
|
@ -1914,6 +1943,27 @@
|
|||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"settings_telemetryModeUpdated": "Modo de telemetria atualizado",
|
||||
"settings_multiAck": "Multi-ACKs: {value}",
|
||||
"map_showOverlaps": "Sobreposições da Chave Repeater",
|
||||
"map_runTraceWithReturnPath": "Retornar ao mesmo caminho.",
|
||||
"@radioStats_noiseFloor": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastRssi": {
|
||||
"placeholders": {
|
||||
"rssiDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastSnr": {
|
||||
"placeholders": {
|
||||
"snr": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
|
|
@ -1922,4 +1972,46 @@
|
|||
"scanner_linuxPairingPinPrompt": "Insira o código PIN para {deviceName} (deixe em branco se não houver).",
|
||||
"scanner_linuxPairingShowPin": "Mostrar PIN",
|
||||
"scanner_linuxPairingHidePin": "Esconder o PIN"
|
||||
"@radioStats_txAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_rxAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_stripNoise": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_jumpToOldestUnread": "Vá para a mensagem mais antiga não lida",
|
||||
"chat_sendCooldown": "Por favor, aguarde um momento antes de reenviar.",
|
||||
"appSettings_languageHu": "Húngaro",
|
||||
"appSettings_jumpToOldestUnreadSubtitle": "Ao abrir uma conversa com mensagens não lidas, role para a primeira mensagem não lida, em vez da mais recente.",
|
||||
"appSettings_languageJa": "Japonês",
|
||||
"appSettings_languageKo": "Coreano",
|
||||
"radioStats_tooltip": "Estatísticas de rádio e malha",
|
||||
"radioStats_screenTitle": "Estatísticas de rádio",
|
||||
"radioStats_notConnected": "Conecte-se a um dispositivo para visualizar estatísticas de rádio.",
|
||||
"radioStats_firmwareTooOld": "As estatísticas de rádio exigem o firmware v8 ou uma versão mais recente.",
|
||||
"radioStats_waiting": "Aguardando dados…",
|
||||
"radioStats_noiseFloor": "Nível de ruído: {noiseDbm} dBm",
|
||||
"radioStats_lastRssi": "Último RSSI: {rssiDbm} dBm",
|
||||
"radioStats_lastSnr": "Último SNR: {snr} dB",
|
||||
"radioStats_txAir": "Tempo de transmissão da TX (total): {seconds} s",
|
||||
"radioStats_rxAir": "Tempo de uso do RX (total): {seconds} s",
|
||||
"radioStats_chartCaption": "Nível de ruído (dBm) em amostras recentes.",
|
||||
"radioStats_stripNoise": "Nível de ruído: {noiseDbm} dBm",
|
||||
"radioStats_stripWaiting": "Obtendo estatísticas de rádio…",
|
||||
"radioStats_settingsTile": "Estatísticas de rádio",
|
||||
"radioStats_settingsSubtitle": "Nível de ruído, RSSI, SNR e tempo de transmissão"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1140,6 +1140,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_maxRouteWeight": "Максимальный допустимый вес маршрута",
|
||||
"appSettings_maxRouteWeightSubtitle": "Максимальный вес, который может быть перевезён по определённому маршруту при успешных доставках.",
|
||||
"appSettings_initialRouteWeightSubtitle": "Начальный вес для новых, только что открытых маршрутов",
|
||||
|
|
@ -1154,6 +1183,27 @@
|
|||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"settings_telemetryModeUpdated": "Режим телеметрии обновлен",
|
||||
"settings_multiAck": "Мульти-ACK: {value}",
|
||||
"map_showOverlaps": "Перекрытия ключа повтора",
|
||||
"map_runTraceWithReturnPath": "Вернуться обратно по тому же пути",
|
||||
"@radioStats_noiseFloor": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastRssi": {
|
||||
"placeholders": {
|
||||
"rssiDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastSnr": {
|
||||
"placeholders": {
|
||||
"snr": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
|
|
@ -1162,4 +1212,46 @@
|
|||
"scanner_linuxPairingPinPrompt": "Введите PIN-код для {deviceName} (оставьте поле пустым, если PIN-код отсутствует).",
|
||||
"scanner_linuxPairingHidePin": "Скрыть PIN-код",
|
||||
"scanner_linuxPairingPinTitle": "PIN для сопряжения устройств по Bluetooth"
|
||||
"@radioStats_txAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_rxAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_stripNoise": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_sendCooldown": "Пожалуйста, подождите немного, прежде чем отправлять сообщение снова.",
|
||||
"appSettings_jumpToOldestUnread": "Перейти к самому старому непрочитанному сообщению",
|
||||
"appSettings_languageHu": "Венгерский",
|
||||
"appSettings_jumpToOldestUnreadSubtitle": "При открытии чата с непрочитанными сообщениями, прокрутите страницу, чтобы увидеть первое непрочитанное сообщение, а не последнее.",
|
||||
"appSettings_languageJa": "Японский",
|
||||
"appSettings_languageKo": "Корейский",
|
||||
"radioStats_tooltip": "Статистика радио и беспроводной сети",
|
||||
"radioStats_screenTitle": "Статистика радиовещания",
|
||||
"radioStats_notConnected": "Подключитесь к устройству, чтобы просмотреть статистику радио.",
|
||||
"radioStats_firmwareTooOld": "Для работы радиостатистики требуется установленная версия прошивки v8 или более новая.",
|
||||
"radioStats_waiting": "Ожидаем данных…",
|
||||
"radioStats_noiseFloor": "Уровень шума: {noiseDbm} дБм",
|
||||
"radioStats_lastRssi": "Последнее значение RSSI: {rssiDbm} дБм",
|
||||
"radioStats_lastSnr": "Последнее значение SNR: {snr} дБ",
|
||||
"radioStats_txAir": "Время эфира на телеканале TX (общее): {seconds} секунд",
|
||||
"radioStats_rxAir": "Общее время использования RX (в секундах): {seconds} с",
|
||||
"radioStats_chartCaption": "Уровень шума (дБм) на основе последних измерений.",
|
||||
"radioStats_stripNoise": "Уровень шума: {noiseDbm} дБм",
|
||||
"radioStats_stripWaiting": "Получение данных о радио…",
|
||||
"radioStats_settingsTile": "Статистика радиовещания",
|
||||
"radioStats_settingsSubtitle": "Уровень шума, RSSI, SNR и время передачи"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1900,6 +1900,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_maxRouteWeightSubtitle": "Maximálna hmotnosť, ktorú môže trás prenášať vďaka úspešným zásielkam.",
|
||||
"appSettings_initialRouteWeightSubtitle": "Počiatočná váha pre nové, objavené cesty",
|
||||
"appSettings_initialRouteWeight": "Počiatočná váha trasy",
|
||||
|
|
@ -1914,6 +1943,27 @@
|
|||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"settings_telemetryModeUpdated": "Režim telemetrie bol aktualizovaný",
|
||||
"settings_multiAck": "Viaceré ACK: {value}",
|
||||
"map_showOverlaps": "Prekrývanie opakovača kľúča",
|
||||
"map_runTraceWithReturnPath": "Vráťte sa späť po tej istej ceste.",
|
||||
"@radioStats_noiseFloor": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastRssi": {
|
||||
"placeholders": {
|
||||
"rssiDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastSnr": {
|
||||
"placeholders": {
|
||||
"snr": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
|
|
@ -1922,4 +1972,46 @@
|
|||
"scanner_linuxPairingShowPin": "Zobraziť PIN",
|
||||
"scanner_linuxPairingHidePin": "Skryť PIN",
|
||||
"scanner_linuxPairingPinTitle": "PIN pre párovanie cez Bluetooth"
|
||||
"@radioStats_txAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_rxAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_stripNoise": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_sendCooldown": "Prosím, počkajte chvíľu, než zašlete znova.",
|
||||
"appSettings_jumpToOldestUnread": "Presk oceň",
|
||||
"appSettings_jumpToOldestUnreadSubtitle": "Pri otvorení chatu s neprečítanými správami, prejdite do prvého neprečítaného, namiesto poslednej.",
|
||||
"appSettings_languageHu": "Maďarský",
|
||||
"appSettings_languageJa": "Japonský",
|
||||
"appSettings_languageKo": "Kórejský",
|
||||
"radioStats_tooltip": "Statistiky rádiových a sieťových kanálov",
|
||||
"radioStats_screenTitle": "Štatistiky rádiových vysielaní",
|
||||
"radioStats_notConnected": "Pripojte sa k zariadeniu, aby ste mohli sledovať štatistiky rádiového vysielania.",
|
||||
"radioStats_firmwareTooOld": "Statistické údaje z rádia vyžadujú sprievodný softvér verzie v8 alebo novšej.",
|
||||
"radioStats_waiting": "Čakám na údaje…",
|
||||
"radioStats_noiseFloor": "Úroveň hluku: {noiseDbm} dBm",
|
||||
"radioStats_lastRssi": "Posledný údaj RSSI: {rssiDbm} dBm",
|
||||
"radioStats_lastSnr": "Posledná hodnota SNR: {snr} dB",
|
||||
"radioStats_txAir": "Čas vysielania na TX (celkový): {seconds} s",
|
||||
"radioStats_rxAir": "Čas RX (celkový): {seconds} s",
|
||||
"radioStats_chartCaption": "Úroveň šumu (dBm) pre posledné vzorky.",
|
||||
"radioStats_stripNoise": "Úroveň hluku: {noiseDbm} dBm",
|
||||
"radioStats_stripWaiting": "Získavanie údajov o rádiu…",
|
||||
"radioStats_settingsTile": "Štatistiky rádiových vysielaní",
|
||||
"radioStats_settingsSubtitle": "Úroveň hluku, RSSI, SNR a časové rozloženie"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1900,6 +1900,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_maxRouteWeightSubtitle": "Največja teža, ki jo lahko pot doseže s uspešnimi dostavnami.",
|
||||
"appSettings_initialRouteWeight": "Izvirna teža poti",
|
||||
"appSettings_initialRouteWeightSubtitle": "Izguba teže za nove, odkriti poti",
|
||||
|
|
@ -1914,6 +1943,27 @@
|
|||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"settings_multiAck": "Večkratni potrditvi: {value}",
|
||||
"settings_telemetryModeUpdated": "Način telemetrije posodobljen",
|
||||
"map_showOverlaps": "Prekrivanje ključa ponovnega predvajanja",
|
||||
"map_runTraceWithReturnPath": "Vrni se nazaj po isti poti.",
|
||||
"@radioStats_noiseFloor": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastRssi": {
|
||||
"placeholders": {
|
||||
"rssiDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastSnr": {
|
||||
"placeholders": {
|
||||
"snr": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
|
|
@ -1922,4 +1972,46 @@
|
|||
"scanner_linuxPairingPinPrompt": "Vnesite PIN kodo za {deviceName} (ostavite prazno, če nimate kode).",
|
||||
"scanner_linuxPairingHidePin": "Skrijte PIN",
|
||||
"scanner_linuxPairingPinTitle": "PIN za združevanje preko Bluetootha"
|
||||
"@radioStats_txAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_rxAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_stripNoise": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_languageHu": "Madžarski",
|
||||
"appSettings_jumpToOldestUnreadSubtitle": "Ko odpirate klepet z neprebranimi sporočili, se premaknite na prvo neprebrano sporočilo, namesto najnovejšega.",
|
||||
"chat_sendCooldown": "Prosimo, počakajte trenutek, preden pošljete ponovno.",
|
||||
"appSettings_jumpToOldestUnread": "Pritisnite za najstarejše nepročitano sporočilo",
|
||||
"appSettings_languageJa": "Japonski",
|
||||
"appSettings_languageKo": "Korejski",
|
||||
"radioStats_tooltip": "Statistike za radio in mrežo",
|
||||
"radioStats_notConnected": "Povežite se z napravo, da si ogledate statistiko o radiju.",
|
||||
"radioStats_screenTitle": "Radijske statistike",
|
||||
"radioStats_firmwareTooOld": "Statistika za radio zahteva združljivo programsko opremo v8 ali kasnejše.",
|
||||
"radioStats_waiting": "Čakam na podatke…",
|
||||
"radioStats_noiseFloor": "Število šuma: {noiseDbm} dBm",
|
||||
"radioStats_lastRssi": "Najkasnejše vrednost RSSI: {rssiDbm} dBm",
|
||||
"radioStats_lastSnr": "Najkasnejše vrednost SNR: {snr} dB",
|
||||
"radioStats_txAir": "Čas na TX (skupno): {seconds} s",
|
||||
"radioStats_rxAir": "Čas, namenjen RX-ju (skupno): {seconds} s",
|
||||
"radioStats_chartCaption": "Ravnovredna raven šuma (dBm) za nedavne vzorce.",
|
||||
"radioStats_stripNoise": "Število šuma: {noiseDbm} dBm",
|
||||
"radioStats_stripWaiting": "Prejemanje statistike o radiju…",
|
||||
"radioStats_settingsTile": "Radijske statistike",
|
||||
"radioStats_settingsSubtitle": "Število šumov, RSSI, SNR in čas, ki ga je napolnila oprema"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1900,6 +1900,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeightSubtitle": "Initial vikt för nyligen upptäckta vägar",
|
||||
"appSettings_maxRouteWeight": "Maximalt tillåtet vikt för rutten",
|
||||
"appSettings_maxRouteWeightSubtitle": "Maximal vikt som en leveransväg kan ackumulera från framgångsrika leveranser.",
|
||||
|
|
@ -1914,6 +1943,27 @@
|
|||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"settings_telemetryModeUpdated": "Telemetri-läge uppdaterat",
|
||||
"settings_multiAck": "Multi-ACKs: {value}",
|
||||
"map_showOverlaps": "Repeater-nyckelöverlappningar",
|
||||
"map_runTraceWithReturnPath": "Gå tillbaka på samma väg",
|
||||
"@radioStats_noiseFloor": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastRssi": {
|
||||
"placeholders": {
|
||||
"rssiDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastSnr": {
|
||||
"placeholders": {
|
||||
"snr": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
|
|
@ -1922,4 +1972,46 @@
|
|||
"scanner_linuxPairingHidePin": "Dölj PIN-kod",
|
||||
"scanner_linuxPairingShowPin": "Visa PIN-kod",
|
||||
"scanner_linuxPairingPinTitle": "PIN för Bluetooth-parning"
|
||||
"@radioStats_txAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_rxAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_stripNoise": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_jumpToOldestUnreadSubtitle": "När du öppnar en chatt med oinlästa meddelanden, scrolla till det första oinlästa meddelandet istället för det senaste.",
|
||||
"chat_sendCooldown": "Vänligen vänta en stund innan du skickar igen.",
|
||||
"appSettings_jumpToOldestUnread": "Gå direkt till det äldsta, obesvarade meddelandet",
|
||||
"appSettings_languageHu": "Ungerskt",
|
||||
"appSettings_languageJa": "Japanska",
|
||||
"appSettings_languageKo": "Koreanska",
|
||||
"radioStats_tooltip": "Radio- och mesh-statistik",
|
||||
"radioStats_screenTitle": "Radiostation",
|
||||
"radioStats_notConnected": "Anslut till en enhet för att visa radiostatistik.",
|
||||
"radioStats_firmwareTooOld": "Radio statistik kräver kompatibel firmware version 8 eller senare.",
|
||||
"radioStats_waiting": "Väntar på data…",
|
||||
"radioStats_noiseFloor": "Bakgrundsnivå: {noiseDbm} dBm",
|
||||
"radioStats_lastRssi": "Senaste RSSI-värde: {rssiDbm} dBm",
|
||||
"radioStats_lastSnr": "Senaste SNR: {snr} dB",
|
||||
"radioStats_txAir": "TX-tid (total): {seconds} sekunder",
|
||||
"radioStats_rxAir": "RX-tid (total): {seconds} s",
|
||||
"radioStats_chartCaption": "Ljudnivå (dBm) baserat på de senaste mätningarna.",
|
||||
"radioStats_stripNoise": "Bakgrundsnivå: {noiseDbm} dBm",
|
||||
"radioStats_stripWaiting": "Hämtar radiostatistik…",
|
||||
"radioStats_settingsTile": "Radiostation",
|
||||
"radioStats_settingsSubtitle": "Bakgrundsnivå, RSSI, SNR och tillgänglig tid"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1900,6 +1900,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_initialRouteWeight": "Початкова вартість маршруту",
|
||||
"appSettings_initialRouteWeightSubtitle": "Початкова вага для нових відкритих шляхів",
|
||||
"appSettings_maxRouteWeight": "Максимальна вага маршруту",
|
||||
|
|
@ -1914,6 +1943,27 @@
|
|||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"settings_telemetryModeUpdated": "Режим телеметрії оновлено",
|
||||
"settings_multiAck": "Багатократне підтвердження: {value}",
|
||||
"map_showOverlaps": "Перекриття ключа повторювача",
|
||||
"map_runTraceWithReturnPath": "Повернутися назад тим же шляхом",
|
||||
"@radioStats_noiseFloor": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastRssi": {
|
||||
"placeholders": {
|
||||
"rssiDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastSnr": {
|
||||
"placeholders": {
|
||||
"snr": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
|
|
@ -1922,4 +1972,46 @@
|
|||
"scanner_linuxPairingPinTitle": "PIN для з'єднання через Bluetooth",
|
||||
"scanner_linuxPairingPinPrompt": "Введіть PIN-код для {deviceName} (залиште поле порожнім, якщо немає).",
|
||||
"scanner_linuxPairingShowPin": "Показати PIN-код"
|
||||
"@radioStats_txAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_rxAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_stripNoise": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_sendCooldown": "Будь ласка, зачекайте трохи, перш ніж відправляти знову.",
|
||||
"appSettings_languageHu": "Угорський",
|
||||
"appSettings_jumpToOldestUnreadSubtitle": "При відкритті чату з не прочитаними повідомленнями, прокрутіть до першого не прочитаного повідомлення, а не до останнього.",
|
||||
"appSettings_jumpToOldestUnread": "Перейти до найстарішого непрочитаного повідомлення",
|
||||
"appSettings_languageJa": "Японська",
|
||||
"appSettings_languageKo": "Кореєська",
|
||||
"radioStats_tooltip": "Статистика радіо та мережі",
|
||||
"radioStats_screenTitle": "Дані про радіостанції",
|
||||
"radioStats_notConnected": "Підключіться до пристрою, щоб переглядати статистику радіопередач.",
|
||||
"radioStats_firmwareTooOld": "Статистика радіо приймача вимагає супутнього програмного забезпечення версії 8 або новішої.",
|
||||
"radioStats_waiting": "Очікую на отримання даних…",
|
||||
"radioStats_noiseFloor": "Рівень шуму: {noiseDbm} дБм",
|
||||
"radioStats_lastRssi": "Останній показник RSSI: {rssiDbm} дБм",
|
||||
"radioStats_lastSnr": "Останній показник SNR: {snr} дБ",
|
||||
"radioStats_txAir": "Час трансляції на телеканалі TX (загальний): {seconds} секунд",
|
||||
"radioStats_rxAir": "Загальний час використання RX: {seconds} секунд",
|
||||
"radioStats_chartCaption": "Рівень шуму (дБм) на основі останніх вимірювань.",
|
||||
"radioStats_stripNoise": "Рівень шуму: {noiseDbm} дБм",
|
||||
"radioStats_stripWaiting": "Отримано статистику радіо…",
|
||||
"radioStats_settingsTile": "Дані про радіостанції",
|
||||
"radioStats_settingsSubtitle": "Рівень шуму, RSSI, SNR та час, протягом якого пристрій використовує радіоканал."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1905,6 +1905,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_maxRouteWeight": "最大路径重量",
|
||||
"appSettings_initialRouteWeightSubtitle": "新发现路径的初始重量",
|
||||
"appSettings_initialRouteWeight": "初始路线权重",
|
||||
|
|
@ -1919,6 +1948,27 @@
|
|||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"settings_multiAck": "多重ACK:{value}",
|
||||
"settings_telemetryModeUpdated": "遥测模式已更新",
|
||||
"map_showOverlaps": "重复键重叠",
|
||||
"map_runTraceWithReturnPath": "沿着相同的路径返回",
|
||||
"@radioStats_noiseFloor": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastRssi": {
|
||||
"placeholders": {
|
||||
"rssiDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_lastSnr": {
|
||||
"placeholders": {
|
||||
"snr": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
|
|
@ -1927,4 +1977,46 @@
|
|||
"scanner_linuxPairingHidePin": "隐藏PIN码",
|
||||
"scanner_linuxPairingPinPrompt": "输入 {deviceName} 的 PIN 码(如果为空,则留空)。",
|
||||
"scanner_linuxPairingShowPin": "显示PIN码"
|
||||
"@radioStats_txAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_rxAir": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radioStats_stripNoise": {
|
||||
"placeholders": {
|
||||
"noiseDbm": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_sendCooldown": "请稍等片刻后再尝试发送。",
|
||||
"appSettings_jumpToOldestUnreadSubtitle": "在打开包含未读消息的聊天时,请滚动到第一个未读消息,而不是最新的消息。",
|
||||
"appSettings_jumpToOldestUnread": "跳转到最旧、未读的文章",
|
||||
"appSettings_languageHu": "匈牙利",
|
||||
"appSettings_languageJa": "日语",
|
||||
"appSettings_languageKo": "韩语",
|
||||
"radioStats_tooltip": "无线电和网状结构统计数据",
|
||||
"radioStats_screenTitle": "广播统计数据",
|
||||
"radioStats_notConnected": "连接到设备以查看收音机统计信息。",
|
||||
"radioStats_firmwareTooOld": "使用无线电统计功能需要配合使用 v8 或更高版本的固件。",
|
||||
"radioStats_waiting": "正在等待数据…",
|
||||
"radioStats_noiseFloor": "噪声水平:{noiseDbm} dBm",
|
||||
"radioStats_lastRssi": "上次 RSSI 值:{rssiDbm} dBm",
|
||||
"radioStats_lastSnr": "上次 SNR:{snr} dB",
|
||||
"radioStats_txAir": "TX 频道播出时间(总时长):{seconds} 秒",
|
||||
"radioStats_rxAir": "RX 使用时长(总时长):{seconds} 秒",
|
||||
"radioStats_chartCaption": "近期的噪声水平(dBm)。",
|
||||
"radioStats_stripNoise": "噪声水平:{noiseDbm} dBm",
|
||||
"radioStats_stripWaiting": "正在获取收音机数据…",
|
||||
"radioStats_settingsTile": "广播统计数据",
|
||||
"radioStats_settingsSubtitle": "噪声水平、RSSI、信噪比和空中时间"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ class AppSettings {
|
|||
final bool mapShowRepeaters;
|
||||
final bool mapShowChatNodes;
|
||||
final bool mapShowOtherNodes;
|
||||
final bool mapShowOverlaps;
|
||||
final double mapTimeFilterHours; // 0 = all time
|
||||
final bool mapKeyPrefixEnabled;
|
||||
final String mapKeyPrefix;
|
||||
|
|
@ -47,12 +48,14 @@ class AppSettings {
|
|||
final bool mapShowDiscoveryContacts;
|
||||
final String tcpServerAddress;
|
||||
final int tcpServerPort;
|
||||
final bool jumpToOldestUnread;
|
||||
|
||||
AppSettings({
|
||||
this.clearPathOnMaxRetry = false,
|
||||
this.mapShowRepeaters = true,
|
||||
this.mapShowChatNodes = true,
|
||||
this.mapShowOtherNodes = true,
|
||||
this.mapShowOverlaps = false,
|
||||
this.mapTimeFilterHours = 0, // Default to all time
|
||||
this.mapKeyPrefixEnabled = false,
|
||||
this.mapKeyPrefix = '',
|
||||
|
|
@ -82,6 +85,7 @@ class AppSettings {
|
|||
this.mapShowDiscoveryContacts = true,
|
||||
this.tcpServerAddress = '',
|
||||
this.tcpServerPort = 0,
|
||||
this.jumpToOldestUnread = false,
|
||||
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
|
||||
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
|
||||
mutedChannels = mutedChannels ?? {};
|
||||
|
|
@ -92,6 +96,7 @@ class AppSettings {
|
|||
'map_show_repeaters': mapShowRepeaters,
|
||||
'map_show_chat_nodes': mapShowChatNodes,
|
||||
'map_show_other_nodes': mapShowOtherNodes,
|
||||
'map_show_overlaps': mapShowOverlaps,
|
||||
'map_time_filter_hours': mapTimeFilterHours,
|
||||
'map_key_prefix_enabled': mapKeyPrefixEnabled,
|
||||
'map_key_prefix': mapKeyPrefix,
|
||||
|
|
@ -121,6 +126,7 @@ class AppSettings {
|
|||
'map_show_discovery_contacts': mapShowDiscoveryContacts,
|
||||
'tcp_server_address': tcpServerAddress,
|
||||
'tcp_server_port': tcpServerPort,
|
||||
'jump_to_oldest_unread': jumpToOldestUnread,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -137,6 +143,7 @@ class AppSettings {
|
|||
mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true,
|
||||
mapShowChatNodes: json['map_show_chat_nodes'] as bool? ?? true,
|
||||
mapShowOtherNodes: json['map_show_other_nodes'] as bool? ?? true,
|
||||
mapShowOverlaps: json['map_show_overlaps'] as bool? ?? false,
|
||||
mapTimeFilterHours:
|
||||
(json['map_time_filter_hours'] as num?)?.toDouble() ?? 0,
|
||||
mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false,
|
||||
|
|
@ -188,6 +195,7 @@ class AppSettings {
|
|||
json['map_show_discovery_contacts'] as bool? ?? true,
|
||||
tcpServerAddress: json['tcp_server_address'] as String? ?? '',
|
||||
tcpServerPort: json['tcp_server_port'] as int? ?? 0,
|
||||
jumpToOldestUnread: json['jump_to_oldest_unread'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -196,6 +204,7 @@ class AppSettings {
|
|||
bool? mapShowRepeaters,
|
||||
bool? mapShowChatNodes,
|
||||
bool? mapShowOtherNodes,
|
||||
bool? mapShowOverlaps,
|
||||
double? mapTimeFilterHours,
|
||||
bool? mapKeyPrefixEnabled,
|
||||
String? mapKeyPrefix,
|
||||
|
|
@ -225,12 +234,14 @@ class AppSettings {
|
|||
bool? mapShowDiscoveryContacts,
|
||||
String? tcpServerAddress,
|
||||
int? tcpServerPort,
|
||||
bool? jumpToOldestUnread,
|
||||
}) {
|
||||
return AppSettings(
|
||||
clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry,
|
||||
mapShowRepeaters: mapShowRepeaters ?? this.mapShowRepeaters,
|
||||
mapShowChatNodes: mapShowChatNodes ?? this.mapShowChatNodes,
|
||||
mapShowOtherNodes: mapShowOtherNodes ?? this.mapShowOtherNodes,
|
||||
mapShowOverlaps: mapShowOverlaps ?? this.mapShowOverlaps,
|
||||
mapTimeFilterHours: mapTimeFilterHours ?? this.mapTimeFilterHours,
|
||||
mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled,
|
||||
mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix,
|
||||
|
|
@ -272,6 +283,7 @@ class AppSettings {
|
|||
mapShowDiscoveryContacts ?? this.mapShowDiscoveryContacts,
|
||||
tcpServerAddress: tcpServerAddress ?? this.tcpServerAddress,
|
||||
tcpServerPort: tcpServerPort ?? this.tcpServerPort,
|
||||
jumpToOldestUnread: jumpToOldestUnread ?? this.jumpToOldestUnread,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,20 +24,23 @@ class Channel {
|
|||
|
||||
bool get isPublicChannel => pskHex == publicChannelPsk;
|
||||
|
||||
static Channel? fromFrame(Uint8List data) {
|
||||
static Channel? fromFrame(Uint8List frame) {
|
||||
// CHANNEL_INFO format:
|
||||
// [0] = RESP_CODE_CHANNEL_INFO (18)
|
||||
// [1] = channel_idx
|
||||
// [2-33] = name (32 bytes, null-terminated)
|
||||
// [34-49] = psk (16 bytes)
|
||||
if (data.length < 50) return null;
|
||||
if (data[0] != respCodeChannelInfo) return null;
|
||||
|
||||
final index = data[1];
|
||||
final name = readCString(data, 2, 32);
|
||||
final psk = Uint8List.fromList(data.sublist(34, 50));
|
||||
|
||||
return Channel(index: index, name: name, psk: psk);
|
||||
if (frame.length < 50) return null;
|
||||
final reader = BufferReader(frame);
|
||||
try {
|
||||
if (reader.readByte() != respCodeChannelInfo) return null;
|
||||
final index = reader.readByte();
|
||||
final name = reader.readCStringGreedy(32);
|
||||
final psk = reader.readBytes(16);
|
||||
return Channel(index: index, name: name, psk: psk);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Channel empty(int index) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:typed_data';
|
|||
import '../connector/meshcore_protocol.dart';
|
||||
import '../helpers/reaction_helper.dart';
|
||||
import '../helpers/smaz.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
|
||||
enum ChannelMessageStatus { pending, sent, failed }
|
||||
|
||||
|
|
@ -109,89 +110,82 @@ class ChannelMessage {
|
|||
);
|
||||
}
|
||||
|
||||
static ChannelMessage? fromFrame(Uint8List data) {
|
||||
static ChannelMessage? fromFrame(Uint8List frame) {
|
||||
// CHANNEL_MSG_RECV format varies by version:
|
||||
// V3: [0]=code [1]=SNR [2]=rsv1 [3]=rsv2 [4]=channel_idx [5]=path_len [path... optional] [txt_type] [timestamp x4] [text...]
|
||||
// Non-V3: [0]=code [1]=channel_idx [2]=path_len [3]=txt_type [4-7]=timestamp [8+]=text
|
||||
if (data.length < 8) return null;
|
||||
if (frame.length < 8) return null;
|
||||
try {
|
||||
final reader = BufferReader(frame);
|
||||
final code = reader.readByte();
|
||||
if (code != respCodeChannelMsgRecv && code != respCodeChannelMsgRecvV3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final code = data[0];
|
||||
if (code != respCodeChannelMsgRecv && code != respCodeChannelMsgRecvV3) {
|
||||
int pathLen;
|
||||
int txtType;
|
||||
Uint8List pathBytes = Uint8List(0);
|
||||
int channelIdx;
|
||||
if (code == respCodeChannelMsgRecvV3) {
|
||||
reader.skipBytes(1); // Skip SNR
|
||||
final flags = reader.readByte();
|
||||
final hasPath = (flags & 0x01) != 0;
|
||||
reader.skipBytes(1); // Skip reserved byte
|
||||
channelIdx = reader.readByte();
|
||||
pathLen = reader.readInt8();
|
||||
txtType = reader.readByte();
|
||||
if (hasPath && pathLen > 0) {
|
||||
reader.rewind(); // Rewind to read path length again for pathBytes
|
||||
pathBytes = reader.readBytes(pathLen);
|
||||
}
|
||||
} else {
|
||||
channelIdx = reader.readByte();
|
||||
pathLen = reader.readInt8();
|
||||
txtType = reader.readByte();
|
||||
}
|
||||
final timestampRaw = reader.readUInt32LE();
|
||||
|
||||
if (txtType != txtTypePlain) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final text = reader.readCString();
|
||||
|
||||
// Extract sender name and actual message from "name: msg" format
|
||||
String senderName = 'Unknown';
|
||||
String actualText = text;
|
||||
|
||||
final colonIndex = text.indexOf(':');
|
||||
if (colonIndex > 0 && colonIndex < text.length - 1 && colonIndex < 50) {
|
||||
final potentialSender = text.substring(0, colonIndex);
|
||||
if (!RegExp(r'[:\[\]]').hasMatch(potentialSender)) {
|
||||
senderName = potentialSender;
|
||||
final offset =
|
||||
(colonIndex + 1 < text.length && text[colonIndex + 1] == ' ')
|
||||
? colonIndex + 2
|
||||
: colonIndex + 1;
|
||||
actualText = text.substring(offset);
|
||||
}
|
||||
}
|
||||
|
||||
final decodedText = Smaz.tryDecodePrefixed(actualText) ?? actualText;
|
||||
|
||||
return ChannelMessage(
|
||||
senderKey: null,
|
||||
senderName: senderName,
|
||||
text: decodedText,
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
|
||||
isOutgoing: false,
|
||||
status: ChannelMessageStatus.sent,
|
||||
pathLength: pathLen,
|
||||
pathBytes: pathBytes,
|
||||
channelIndex: channelIdx,
|
||||
);
|
||||
} catch (e) {
|
||||
appLogger.error('Error parsing channel message frame: $e');
|
||||
// If parsing fails, return null to avoid crashes
|
||||
return null;
|
||||
}
|
||||
|
||||
int timestampOffset, textOffset, pathLenOffset, txtTypeOffset;
|
||||
Uint8List pathBytes = Uint8List(0);
|
||||
int channelIdx;
|
||||
|
||||
if (code == respCodeChannelMsgRecvV3) {
|
||||
channelIdx = data[4];
|
||||
pathLenOffset = 5;
|
||||
final pathLen = data[pathLenOffset].toSigned(8);
|
||||
var cursor = 6;
|
||||
final hasPathBytesFlag = (data[2] & 0x01) != 0;
|
||||
final canFitPath = pathLen > 0 && data.length >= cursor + pathLen + 5;
|
||||
final hasValidTxtType =
|
||||
cursor < data.length &&
|
||||
(data[cursor] == txtTypePlain || data[cursor] == txtTypeCliData);
|
||||
if ((hasPathBytesFlag || (canFitPath && !hasValidTxtType)) &&
|
||||
canFitPath) {
|
||||
pathBytes = Uint8List.fromList(data.sublist(cursor, cursor + pathLen));
|
||||
cursor += pathLen;
|
||||
}
|
||||
txtTypeOffset = cursor;
|
||||
cursor += 1; // txt_type
|
||||
timestampOffset = cursor;
|
||||
textOffset = cursor + 4;
|
||||
} else {
|
||||
channelIdx = data[1];
|
||||
pathLenOffset = 2;
|
||||
txtTypeOffset = 3;
|
||||
timestampOffset = 4;
|
||||
textOffset = 8;
|
||||
}
|
||||
|
||||
if (data.length < textOffset + 1) return null;
|
||||
|
||||
final txtType = data[txtTypeOffset];
|
||||
if (txtType != txtTypePlain) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final pathLen = data[pathLenOffset].toSigned(8);
|
||||
final timestampRaw = readUint32LE(data, timestampOffset);
|
||||
final text = readCString(data, textOffset, data.length - textOffset);
|
||||
|
||||
// Extract sender name and actual message from "name: msg" format
|
||||
String senderName = 'Unknown';
|
||||
String actualText = text;
|
||||
|
||||
final colonIndex = text.indexOf(':');
|
||||
if (colonIndex > 0 && colonIndex < text.length - 1 && colonIndex < 50) {
|
||||
final potentialSender = text.substring(0, colonIndex);
|
||||
if (!RegExp(r'[:\[\]]').hasMatch(potentialSender)) {
|
||||
senderName = potentialSender;
|
||||
final offset =
|
||||
(colonIndex + 1 < text.length && text[colonIndex + 1] == ' ')
|
||||
? colonIndex + 2
|
||||
: colonIndex + 1;
|
||||
actualText = text.substring(offset);
|
||||
}
|
||||
}
|
||||
|
||||
final decodedText = Smaz.tryDecodePrefixed(actualText) ?? actualText;
|
||||
|
||||
return ChannelMessage(
|
||||
senderKey: null,
|
||||
senderName: senderName,
|
||||
text: decodedText,
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
|
||||
isOutgoing: false,
|
||||
status: ChannelMessageStatus.sent,
|
||||
pathLength: pathLen,
|
||||
pathBytes: pathBytes,
|
||||
channelIndex: channelIdx,
|
||||
);
|
||||
}
|
||||
|
||||
static ChannelMessage outgoing(
|
||||
|
|
|
|||
48
lib/models/companion_radio_stats.dart
Normal file
48
lib/models/companion_radio_stats.dart
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
|
||||
/// Parsed `RESP_CODE_STATS` + `STATS_TYPE_RADIO` (14 bytes total).
|
||||
class CompanionRadioStats {
|
||||
final int noiseFloorDbm;
|
||||
final int lastRssiDbm;
|
||||
final double lastSnrDb;
|
||||
final int txAirSecs;
|
||||
final int rxAirSecs;
|
||||
final DateTime receivedAt;
|
||||
|
||||
const CompanionRadioStats({
|
||||
required this.noiseFloorDbm,
|
||||
required this.lastRssiDbm,
|
||||
required this.lastSnrDb,
|
||||
required this.txAirSecs,
|
||||
required this.rxAirSecs,
|
||||
required this.receivedAt,
|
||||
});
|
||||
|
||||
static CompanionRadioStats? tryParse(Uint8List frame) {
|
||||
if (frame.length < 14) return null;
|
||||
if (frame[0] != respCodeStats || frame[1] != statsTypeRadio) return null;
|
||||
try {
|
||||
final reader = BufferReader(frame);
|
||||
reader.skipBytes(2);
|
||||
final noise = reader.readInt16LE();
|
||||
final rssi = reader.readInt8();
|
||||
final snrRaw = reader.readInt8();
|
||||
final txAir = reader.readUInt32LE();
|
||||
final rxAir = reader.readUInt32LE();
|
||||
return CompanionRadioStats(
|
||||
noiseFloorDbm: noise,
|
||||
lastRssiDbm: rssi,
|
||||
lastSnrDb: snrRaw / 4.0,
|
||||
txAirSecs: txAir,
|
||||
rxAirSecs: rxAir,
|
||||
receivedAt: DateTime.now(),
|
||||
);
|
||||
} catch (e) {
|
||||
appLogger.warn('CompanionRadioStats parse error: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ class Contact {
|
|||
final DateTime lastSeen;
|
||||
final DateTime lastMessageAt;
|
||||
final bool isActive;
|
||||
final bool wasPulled;
|
||||
final Uint8List? rawPacket;
|
||||
|
||||
Contact({
|
||||
|
|
@ -34,6 +35,7 @@ class Contact {
|
|||
required this.lastSeen,
|
||||
DateTime? lastMessageAt,
|
||||
this.isActive = true,
|
||||
this.wasPulled = false,
|
||||
this.rawPacket,
|
||||
}) : lastMessageAt = lastMessageAt ?? lastSeen;
|
||||
|
||||
|
|
@ -117,15 +119,14 @@ class Contact {
|
|||
);
|
||||
}
|
||||
|
||||
String get pathIdList {
|
||||
/// Formats path bytes into comma-separated hex groups of [hashByteWidth] bytes.
|
||||
String pathFormattedIdList(int hashByteWidth) {
|
||||
final pathBytes = pathBytesForDisplay;
|
||||
if (pathBytes.isEmpty) return '';
|
||||
final w = hashByteWidth.clamp(1, 8);
|
||||
final parts = <String>[];
|
||||
final groupSize = pathHashSize;
|
||||
for (int i = 0; i < pathBytes.length; i += groupSize) {
|
||||
final end = (i + groupSize) <= pathBytes.length
|
||||
? (i + groupSize)
|
||||
: pathBytes.length;
|
||||
for (int i = 0; i < pathBytes.length; i += w) {
|
||||
final end = (i + w) <= pathBytes.length ? (i + w) : pathBytes.length;
|
||||
final chunk = pathBytes.sublist(i, end);
|
||||
parts.add(
|
||||
chunk
|
||||
|
|
@ -136,6 +137,9 @@ class Contact {
|
|||
return parts.join(',');
|
||||
}
|
||||
|
||||
/// Default grouping uses legacy single-byte hop hash width.
|
||||
String get pathIdList => pathFormattedIdList(pathHashSize);
|
||||
|
||||
String get shortPubKeyHex {
|
||||
return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>";
|
||||
}
|
||||
|
|
@ -181,12 +185,13 @@ class Contact {
|
|||
final lastMod = reader.readUInt32LE();
|
||||
|
||||
double? lat, lon;
|
||||
final latRaw = reader.readInt32LE();
|
||||
final lonRaw = reader.readInt32LE();
|
||||
|
||||
if (latRaw != 0 || lonRaw != 0) {
|
||||
lat = latRaw / 1e6;
|
||||
lon = lonRaw / 1e6;
|
||||
if (reader.remaining >= 8) {
|
||||
final latRaw = reader.readInt32LE();
|
||||
final lonRaw = reader.readInt32LE();
|
||||
if (latRaw != 0 || lonRaw != 0) {
|
||||
lat = latRaw / 1e6;
|
||||
lon = lonRaw / 1e6;
|
||||
}
|
||||
}
|
||||
|
||||
return Contact(
|
||||
|
|
@ -214,4 +219,7 @@ class Contact {
|
|||
|
||||
@override
|
||||
int get hashCode => publicKeyHex.hashCode;
|
||||
bool get teleBaseEnabled => (flags & contactFlagTeleBase) != 0;
|
||||
bool get teleLocEnabled => (flags & contactFlagTeleLoc) != 0;
|
||||
bool get teleEnvEnabled => (flags & contactFlagTeleEnv) != 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class Message {
|
|||
final String? messageId;
|
||||
final int retryCount;
|
||||
final int? estimatedTimeoutMs;
|
||||
final Uint8List? expectedAckHash;
|
||||
final int? expectedAckHash;
|
||||
final DateTime? sentAt;
|
||||
final DateTime? deliveredAt;
|
||||
final int? tripTimeMs;
|
||||
|
|
@ -56,7 +56,7 @@ class Message {
|
|||
MessageStatus? status,
|
||||
int? retryCount,
|
||||
int? estimatedTimeoutMs,
|
||||
Uint8List? expectedAckHash,
|
||||
int? expectedAckHash,
|
||||
DateTime? sentAt,
|
||||
DateTime? deliveredAt,
|
||||
int? tripTimeMs,
|
||||
|
|
@ -90,33 +90,35 @@ class Message {
|
|||
);
|
||||
}
|
||||
|
||||
static Message? fromFrame(Uint8List data, Uint8List selfPubKey) {
|
||||
if (data.length < msgTextOffset + 1) return null;
|
||||
static Message? fromFrame(Uint8List frame, Uint8List selfPubKey) {
|
||||
if (frame.length < msgTextOffset + 1) return null;
|
||||
final reader = BufferReader(frame);
|
||||
try {
|
||||
final code = reader.readByte();
|
||||
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final code = data[0];
|
||||
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
|
||||
final senderKey = reader.readBytes(pubKeySize);
|
||||
final timestampRaw = reader.readInt32LE();
|
||||
final flags = reader.readByte();
|
||||
if ((flags >> 2) != txtTypePlain) {
|
||||
return null;
|
||||
}
|
||||
final text = reader.readCString();
|
||||
|
||||
return Message(
|
||||
senderKey: senderKey,
|
||||
text: text,
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
|
||||
isOutgoing: false,
|
||||
isCli: false,
|
||||
status: MessageStatus.delivered,
|
||||
pathBytes: Uint8List(0),
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final senderKey = Uint8List.fromList(
|
||||
data.sublist(msgPubKeyOffset, msgPubKeyOffset + pubKeySize),
|
||||
);
|
||||
final timestampRaw = readUint32LE(data, msgTimestampOffset);
|
||||
final flags = data[msgFlagsOffset];
|
||||
if ((flags >> 2) != txtTypePlain) {
|
||||
return null;
|
||||
}
|
||||
final text = readCString(data, msgTextOffset, data.length - msgTextOffset);
|
||||
|
||||
return Message(
|
||||
senderKey: senderKey,
|
||||
text: text,
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
|
||||
isOutgoing: false,
|
||||
isCli: false,
|
||||
status: MessageStatus.delivered,
|
||||
pathBytes: Uint8List(0),
|
||||
);
|
||||
}
|
||||
|
||||
static Message outgoing(
|
||||
|
|
|
|||
|
|
@ -291,6 +291,14 @@ class AppSettingsScreen extends StatelessWidget {
|
|||
},
|
||||
),
|
||||
const Divider(height: 1),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.vertical_align_top),
|
||||
title: Text(context.l10n.appSettings_jumpToOldestUnread),
|
||||
subtitle: Text(context.l10n.appSettings_jumpToOldestUnreadSubtitle),
|
||||
value: settingsService.settings.jumpToOldestUnread,
|
||||
onChanged: settingsService.setJumpToOldestUnread,
|
||||
),
|
||||
const Divider(height: 1),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.alt_route),
|
||||
title: Text(context.l10n.appSettings_autoRouteRotation),
|
||||
|
|
@ -689,6 +697,12 @@ class AppSettingsScreen extends StatelessWidget {
|
|||
return context.l10n.appSettings_languageRu;
|
||||
case 'uk':
|
||||
return context.l10n.appSettings_languageUk;
|
||||
case 'hu':
|
||||
return context.l10n.appSettings_languageHu;
|
||||
case 'ja':
|
||||
return context.l10n.appSettings_languageJa;
|
||||
case 'ko':
|
||||
return context.l10n.appSettings_languageKo;
|
||||
default:
|
||||
return context.l10n.appSettings_languageSystem;
|
||||
}
|
||||
|
|
@ -776,6 +790,18 @@ class AppSettingsScreen extends StatelessWidget {
|
|||
title: Text(context.l10n.appSettings_languageUk),
|
||||
value: 'uk',
|
||||
),
|
||||
RadioListTile<String?>(
|
||||
title: Text(context.l10n.appSettings_languageHu),
|
||||
value: 'hu',
|
||||
),
|
||||
RadioListTile<String?>(
|
||||
title: Text(context.l10n.appSettings_languageJa),
|
||||
value: 'ja',
|
||||
),
|
||||
RadioListTile<String?>(
|
||||
title: Text(context.l10n.appSettings_languageKo),
|
||||
value: 'ko',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -283,66 +283,66 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
|||
if (payload.length < 101) {
|
||||
return 'ADVERT (short)';
|
||||
}
|
||||
var offset = 0;
|
||||
final pubKey = _bytesToHex(
|
||||
payload.sublist(offset, offset + 32),
|
||||
spaced: false,
|
||||
);
|
||||
offset += 32;
|
||||
final timestamp = readUint32LE(payload, offset);
|
||||
offset += 4;
|
||||
offset += 64; // signature
|
||||
final flags = payload[offset++];
|
||||
final role = _deviceRoleLabel(flags & 0x0F);
|
||||
final hasLocation = (flags & 0x10) != 0;
|
||||
final hasFeature1 = (flags & 0x20) != 0;
|
||||
final hasFeature2 = (flags & 0x40) != 0;
|
||||
final hasName = (flags & 0x80) != 0;
|
||||
String? name;
|
||||
double? lat;
|
||||
double? lon;
|
||||
if (hasLocation && payload.length >= offset + 8) {
|
||||
lat = readInt32LE(payload, offset) / 1000000.0;
|
||||
lon = readInt32LE(payload, offset + 4) / 1000000.0;
|
||||
offset += 8;
|
||||
final reader = BufferReader(payload);
|
||||
try {
|
||||
final pubKey = _bytesToHex(reader.readBytes(pubKeySize), spaced: false);
|
||||
|
||||
final timestamp = reader.readUInt32LE();
|
||||
reader.skipBytes(signatureSize);
|
||||
final flags = reader.readByte();
|
||||
final role = _deviceRoleLabel(flags & 0x0F);
|
||||
final hasLocation = (flags & 0x10) != 0;
|
||||
final hasFeature1 = (flags & 0x20) != 0;
|
||||
final hasFeature2 = (flags & 0x40) != 0;
|
||||
final hasName = (flags & 0x80) != 0;
|
||||
String? name;
|
||||
double? lat;
|
||||
double? lon;
|
||||
if (hasLocation) {
|
||||
lat = reader.readInt32LE() / 1000000.0;
|
||||
lon = reader.readInt32LE() / 1000000.0;
|
||||
}
|
||||
if (hasFeature1) reader.skipBytes(2);
|
||||
if (hasFeature2) reader.skipBytes(2);
|
||||
if (hasName) {
|
||||
name = reader.readCStringGreedy(maxNameSize);
|
||||
}
|
||||
final namePart = (name != null && name.isNotEmpty) ? ' name="$name"' : '';
|
||||
final locPart = (lat != null && lon != null)
|
||||
? ' loc=${lat.toStringAsFixed(6)},${lon.toStringAsFixed(6)}'
|
||||
: '';
|
||||
return 'ADVERT role=$role ts=$timestamp$namePart$locPart key=${pubKey.substring(0, 12)}…';
|
||||
} catch (e) {
|
||||
return 'ADVERT (invalid)';
|
||||
}
|
||||
if (hasFeature1) offset += 2;
|
||||
if (hasFeature2) offset += 2;
|
||||
if (hasName && payload.length > offset) {
|
||||
final rawName = String.fromCharCodes(payload.sublist(offset));
|
||||
final nul = rawName.indexOf('\u0000');
|
||||
name = nul >= 0 ? rawName.substring(0, nul) : rawName;
|
||||
name = name.trim();
|
||||
}
|
||||
final namePart = (name != null && name.isNotEmpty) ? ' name="$name"' : '';
|
||||
final locPart = (lat != null && lon != null)
|
||||
? ' loc=${lat.toStringAsFixed(6)},${lon.toStringAsFixed(6)}'
|
||||
: '';
|
||||
return 'ADVERT role=$role ts=$timestamp$namePart$locPart key=${pubKey.substring(0, 12)}…';
|
||||
}
|
||||
|
||||
String _decodeControlSummary(Uint8List payload) {
|
||||
if (payload.isEmpty) return 'CONTROL (empty)';
|
||||
final flags = payload[0];
|
||||
final subType = flags & 0xF0;
|
||||
if (subType == 0x80) {
|
||||
if (payload.length < 6) return 'CONTROL DISCOVER_REQ (short)';
|
||||
final typeFilter = payload[1];
|
||||
final tag = readUint32LE(payload, 2);
|
||||
final since = payload.length >= 10 ? readUint32LE(payload, 6) : 0;
|
||||
return 'CONTROL DISCOVER_REQ filter=0x${typeFilter.toRadixString(16).padLeft(2, '0')} tag=$tag since=$since';
|
||||
final reader = BufferReader(payload);
|
||||
try {
|
||||
final flags = reader.readByte();
|
||||
final subType = flags & 0xF0;
|
||||
if (subType == 0x80) {
|
||||
if (payload.length < 6) return 'CONTROL DISCOVER_REQ (short)';
|
||||
final typeFilter = reader.readByte();
|
||||
final tag = reader.readInt32LE();
|
||||
final since = payload.length >= 10 ? reader.readInt32LE() : 0;
|
||||
return 'CONTROL DISCOVER_REQ filter=0x${typeFilter.toRadixString(16).padLeft(2, '0')} tag=$tag since=$since';
|
||||
}
|
||||
if (subType == 0x90) {
|
||||
if (payload.length < 14) return 'CONTROL DISCOVER_RESP (short)';
|
||||
final nodeType = flags & 0x0F;
|
||||
final snrRaw = payload[1];
|
||||
final snrSigned = snrRaw > 127 ? snrRaw - 256 : snrRaw;
|
||||
final snr = snrSigned / 4.0;
|
||||
final tag = reader.readInt32LE();
|
||||
final keyLen = payload.length - 6;
|
||||
return 'CONTROL DISCOVER_RESP node=${_deviceRoleLabel(nodeType)} snr=${snr.toStringAsFixed(2)} tag=$tag key=$keyLen';
|
||||
}
|
||||
return 'CONTROL subtype=0x${subType.toRadixString(16).padLeft(2, '0')}';
|
||||
} catch (e) {
|
||||
return 'CONTROL (invalid)';
|
||||
}
|
||||
if (subType == 0x90) {
|
||||
if (payload.length < 14) return 'CONTROL DISCOVER_RESP (short)';
|
||||
final nodeType = flags & 0x0F;
|
||||
final snrRaw = payload[1];
|
||||
final snrSigned = snrRaw > 127 ? snrRaw - 256 : snrRaw;
|
||||
final snr = snrSigned / 4.0;
|
||||
final tag = readUint32LE(payload, 2);
|
||||
final keyLen = payload.length - 6;
|
||||
return 'CONTROL DISCOVER_RESP node=${_deviceRoleLabel(nodeType)} snr=${snr.toStringAsFixed(2)} tag=$tag key=$keyLen';
|
||||
}
|
||||
return 'CONTROL subtype=0x${subType.toRadixString(16).padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
String _payloadTypeLabel(int payloadType) {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import '../widgets/gif_message.dart';
|
|||
import '../widgets/jump_to_bottom_button.dart';
|
||||
import '../widgets/gif_picker.dart';
|
||||
import '../widgets/message_status_icon.dart';
|
||||
import '../widgets/radio_stats_entry.dart';
|
||||
import 'channel_message_path_screen.dart';
|
||||
import 'map_screen.dart';
|
||||
|
||||
|
|
@ -47,6 +48,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||
bool _isLoadingOlder = false;
|
||||
|
||||
MeshCoreConnector? _connector;
|
||||
DateTime? _lastChannelSendAt;
|
||||
bool _channelSkipNextBottomSnap = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -55,11 +58,45 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||
_scrollController.onScrollNearTop = _loadOlderMessages;
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
_connector = context.read<MeshCoreConnector>();
|
||||
_connector?.setActiveChannel(widget.channel.index);
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final settings = context.read<AppSettingsService>().settings;
|
||||
final idx = widget.channel.index;
|
||||
final unread = connector.getUnreadCountForChannelIndex(idx);
|
||||
ChannelMessage? anchor;
|
||||
if (settings.jumpToOldestUnread && unread > 0) {
|
||||
anchor = _findOldestUnreadChannelAnchor(
|
||||
connector.getChannelMessages(widget.channel),
|
||||
unread,
|
||||
);
|
||||
}
|
||||
connector.setActiveChannel(idx);
|
||||
_connector = connector;
|
||||
if (anchor != null) {
|
||||
_channelSkipNextBottomSnap = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
_scrollToMessage(anchor!.messageId);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ChannelMessage? _findOldestUnreadChannelAnchor(
|
||||
List<ChannelMessage> messages,
|
||||
int unreadCount,
|
||||
) {
|
||||
if (unreadCount <= 0 || messages.isEmpty) return null;
|
||||
var n = 0;
|
||||
ChannelMessage? oldest;
|
||||
for (final m in messages.reversed) {
|
||||
if (m.isOutgoing) continue;
|
||||
n++;
|
||||
oldest = m;
|
||||
if (n >= unreadCount) break;
|
||||
}
|
||||
return oldest;
|
||||
}
|
||||
|
||||
void _onTextFieldFocusChange() {
|
||||
if (_textFieldFocusNode.hasFocus && mounted) {
|
||||
_scrollController.handleKeyboardOpen();
|
||||
|
|
@ -166,6 +203,34 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||
],
|
||||
),
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
const RadioStatsIconButton(),
|
||||
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(
|
||||
top: false,
|
||||
|
|
@ -216,6 +281,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||
|
||||
// Auto-scroll to bottom if user is already at bottom
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_channelSkipNextBottomSnap) {
|
||||
_channelSkipNextBottomSnap = false;
|
||||
return;
|
||||
}
|
||||
_scrollController.scrollToBottomIfAtBottom();
|
||||
});
|
||||
|
||||
|
|
@ -441,11 +510,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||
style: TextStyle(
|
||||
fontSize: bodyFontSize * textScale,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
fontSize: bodyFontSize * textScale,
|
||||
color: Colors.green,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!enableTracing && isOutgoing) ...[
|
||||
|
|
@ -1052,6 +1116,16 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||
final text = _textController.text.trim();
|
||||
if (text.isEmpty) return;
|
||||
|
||||
final now = DateTime.now();
|
||||
if (_lastChannelSendAt != null &&
|
||||
now.difference(_lastChannelSendAt!) < const Duration(seconds: 1)) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(context.l10n.chat_sendCooldown)));
|
||||
return;
|
||||
}
|
||||
_lastChannelSendAt = now;
|
||||
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
|
||||
String messageText = text;
|
||||
|
|
|
|||
|
|
@ -40,8 +40,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||
final primaryPath = !channelMessage && !message.isOutgoing
|
||||
? Uint8List.fromList(primaryPathTmp.reversed.toList())
|
||||
: primaryPathTmp;
|
||||
final contacts = connector.allContacts;
|
||||
final hops = _buildPathHops(primaryPath, contacts, l10n);
|
||||
final hops = _buildPathHops(primaryPath, connector, l10n);
|
||||
final hasHopDetails = primaryPath.isNotEmpty;
|
||||
final observedLabel = _formatObservedHops(
|
||||
primaryPath.length,
|
||||
|
|
@ -65,6 +64,9 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||
flipPathAround: true,
|
||||
reversePathAround:
|
||||
!(!channelMessage && !message.isOutgoing),
|
||||
pathHashByteWidth: context
|
||||
.read<MeshCoreConnector>()
|
||||
.pathHashByteWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -303,10 +305,12 @@ class _ChannelMessagePathMapScreenState
|
|||
extends State<ChannelMessagePathMapScreen> {
|
||||
static const double _labelZoomThreshold = 8.5;
|
||||
|
||||
final MapController _mapController = MapController();
|
||||
Uint8List? _selectedPath;
|
||||
double _pathDistance = 0.0;
|
||||
bool _showNodeLabels = true;
|
||||
bool _didReceivePositionUpdate = false;
|
||||
int? _focusedHopIndex;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -337,6 +341,22 @@ class _ChannelMessagePathMapScreenState
|
|||
return totalDistance;
|
||||
}
|
||||
|
||||
void _focusHop(_PathHop hop) {
|
||||
if (!hop.hasLocation) return;
|
||||
final targetZoom = _didReceivePositionUpdate
|
||||
? max(_mapController.camera.zoom, 10.0)
|
||||
: 12.0;
|
||||
_mapController.move(hop.position!, targetZoom);
|
||||
}
|
||||
|
||||
void _onHopTapped(_PathHop hop) {
|
||||
_focusHop(hop);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_focusedHopIndex = hop.index;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<MeshCoreConnector>(
|
||||
|
|
@ -365,8 +385,7 @@ class _ChannelMessagePathMapScreenState
|
|||
: selectedPathTmp;
|
||||
|
||||
final selectedIndex = _indexForPath(selectedPath, observedPaths);
|
||||
final contacts = connector.allContacts;
|
||||
final hops = _buildPathHops(selectedPath, contacts, context.l10n);
|
||||
final hops = _buildPathHops(selectedPath, connector, context.l10n);
|
||||
|
||||
final points = <LatLng>[];
|
||||
|
||||
|
|
@ -421,6 +440,7 @@ class _ChannelMessagePathMapScreenState
|
|||
children: [
|
||||
FlutterMap(
|
||||
key: mapKey,
|
||||
mapController: _mapController,
|
||||
options: MapOptions(
|
||||
initialCenter: initialCenter,
|
||||
initialZoom: initialZoom,
|
||||
|
|
@ -472,6 +492,7 @@ class _ChannelMessagePathMapScreenState
|
|||
) {
|
||||
setState(() {
|
||||
_selectedPath = observedPaths[index].pathBytes;
|
||||
_focusedHopIndex = null;
|
||||
});
|
||||
}),
|
||||
if (points.isEmpty)
|
||||
|
|
@ -727,8 +748,17 @@ class _ChannelMessagePathMapScreenState
|
|||
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final hop = hops[index];
|
||||
final isFocused = _focusedHopIndex == hop.index;
|
||||
return ListTile(
|
||||
dense: true,
|
||||
enabled: hop.hasLocation,
|
||||
selected: isFocused,
|
||||
selectedTileColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withValues(alpha: 0.12),
|
||||
onTap: hop.hasLocation
|
||||
? () => _onHopTapped(hop)
|
||||
: null,
|
||||
leading: CircleAvatar(
|
||||
radius: 14,
|
||||
child: Text(
|
||||
|
|
@ -787,19 +817,71 @@ class _ObservedPath {
|
|||
|
||||
List<_PathHop> _buildPathHops(
|
||||
Uint8List pathBytes,
|
||||
List<Contact> contacts,
|
||||
MeshCoreConnector connector,
|
||||
AppLocalizations l10n,
|
||||
) {
|
||||
if (pathBytes.isEmpty) return const [];
|
||||
final candidatesByPrefix = <int, List<Contact>>{};
|
||||
for (final contact in connector.allContacts) {
|
||||
if (contact.publicKey.isEmpty) continue;
|
||||
if (contact.type != advTypeRepeater && contact.type != advTypeRoom) {
|
||||
continue;
|
||||
}
|
||||
final prefix = contact.publicKey.first;
|
||||
candidatesByPrefix.putIfAbsent(prefix, () => <Contact>[]).add(contact);
|
||||
}
|
||||
for (final candidates in candidatesByPrefix.values) {
|
||||
candidates.sort((a, b) => b.lastSeen.compareTo(a.lastSeen));
|
||||
}
|
||||
final startPoint =
|
||||
(connector.selfLatitude != null && connector.selfLongitude != null)
|
||||
? LatLng(connector.selfLatitude!, connector.selfLongitude!)
|
||||
: null;
|
||||
var previousPosition = startPoint;
|
||||
final distance = Distance();
|
||||
|
||||
final hops = <_PathHop>[];
|
||||
for (var i = 0; i < pathBytes.length; i++) {
|
||||
final prefix = pathBytes[i];
|
||||
final contact = _matchContactForPrefix(contacts, prefix);
|
||||
final searchPoint = i == 0 ? startPoint : previousPosition;
|
||||
final candidates = candidatesByPrefix[pathBytes[i]];
|
||||
Contact? contact;
|
||||
if (candidates != null && candidates.isNotEmpty) {
|
||||
var bestIndex = 0;
|
||||
if (searchPoint != null) {
|
||||
var bestDistance = double.infinity;
|
||||
for (var j = 0; j < candidates.length; j++) {
|
||||
final candidate = candidates[j];
|
||||
if (!candidate.hasLocation ||
|
||||
candidate.latitude == null ||
|
||||
candidate.longitude == null) {
|
||||
continue;
|
||||
}
|
||||
final currentDistance = distance(
|
||||
searchPoint,
|
||||
LatLng(candidate.latitude!, candidate.longitude!),
|
||||
);
|
||||
if (currentDistance < bestDistance) {
|
||||
bestDistance = currentDistance;
|
||||
bestIndex = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
contact = candidates.removeAt(bestIndex);
|
||||
if (candidates.isEmpty) {
|
||||
candidatesByPrefix.remove(pathBytes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
final resolvedPosition = _resolvePosition(contact);
|
||||
if (resolvedPosition != null) {
|
||||
previousPosition = resolvedPosition;
|
||||
}
|
||||
hops.add(
|
||||
_PathHop(
|
||||
index: i + 1,
|
||||
prefix: prefix,
|
||||
prefix: pathBytes[i],
|
||||
contact: contact,
|
||||
position: _resolvePosition(contact),
|
||||
position: resolvedPosition,
|
||||
l10n: l10n,
|
||||
),
|
||||
);
|
||||
|
|
@ -807,42 +889,13 @@ List<_PathHop> _buildPathHops(
|
|||
return hops;
|
||||
}
|
||||
|
||||
Contact? _matchContactForPrefix(List<Contact> contacts, int prefix) {
|
||||
final matches = contacts
|
||||
.where(
|
||||
(contact) =>
|
||||
(contact.type == advTypeRepeater || contact.type == advTypeRoom) &&
|
||||
contact.publicKey.isNotEmpty &&
|
||||
contact.publicKey[0] == prefix,
|
||||
)
|
||||
.toList();
|
||||
if (matches.isEmpty) return null;
|
||||
|
||||
Contact? pickWhere(bool Function(Contact) predicate) {
|
||||
for (final contact in matches) {
|
||||
if (predicate(contact)) return contact;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return pickWhere((c) => c.type == advTypeRepeater && _hasValidLocation(c)) ??
|
||||
pickWhere((c) => c.type == advTypeRepeater) ??
|
||||
pickWhere(_hasValidLocation) ??
|
||||
matches.first;
|
||||
}
|
||||
|
||||
LatLng? _resolvePosition(Contact? contact) {
|
||||
if (contact == null) return null;
|
||||
if (!_hasValidLocation(contact)) return null;
|
||||
return LatLng(contact.latitude!, contact.longitude!);
|
||||
}
|
||||
|
||||
bool _hasValidLocation(Contact contact) {
|
||||
final lat = contact.latitude;
|
||||
final lon = contact.longitude;
|
||||
if (lat == null || lon == null) return false;
|
||||
if (lat == 0 && lon == 0) return false;
|
||||
return true;
|
||||
if (!contact.hasLocation) return null;
|
||||
final latitude = contact.latitude;
|
||||
final longitude = contact.longitude;
|
||||
if (latitude == null || longitude == null) return null;
|
||||
return LatLng(latitude, longitude);
|
||||
}
|
||||
|
||||
String _formatPrefix(int prefix) {
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||
canPop: allowBack,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: AppBarTitle(context.l10n.channels_title),
|
||||
title: AppBarTitle(context.l10n.channels_title, indicators: false),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
actions: [
|
||||
|
|
|
|||
|
|
@ -36,8 +36,10 @@ import '../widgets/gif_message.dart';
|
|||
import '../widgets/jump_to_bottom_button.dart';
|
||||
import '../widgets/gif_picker.dart';
|
||||
import '../widgets/path_selection_dialog.dart';
|
||||
import '../widgets/radio_stats_entry.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import 'telemetry_screen.dart';
|
||||
|
||||
class ChatScreen extends StatefulWidget {
|
||||
final Contact contact;
|
||||
|
|
@ -52,8 +54,11 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
final _textController = TextEditingController();
|
||||
final _scrollController = ChatScrollController();
|
||||
final _textFieldFocusNode = FocusNode();
|
||||
final GlobalKey _unreadScrollKey = GlobalKey();
|
||||
bool _isLoadingOlder = false;
|
||||
MeshCoreConnector? _connector;
|
||||
Message? _pendingUnreadScrollTarget;
|
||||
DateTime? _lastTextSendAt;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -62,11 +67,50 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
_scrollController.onScrollNearTop = _loadOlderMessages;
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
_connector = context.read<MeshCoreConnector>();
|
||||
_connector?.setActiveContact(widget.contact.publicKeyHex);
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final settings = context.read<AppSettingsService>().settings;
|
||||
final keyHex = widget.contact.publicKeyHex;
|
||||
final unread = connector.getUnreadCountForContactKey(keyHex);
|
||||
Message? anchor;
|
||||
if (settings.jumpToOldestUnread && unread > 0) {
|
||||
anchor = _findOldestUnreadAnchor(
|
||||
connector.getMessages(widget.contact),
|
||||
unread,
|
||||
);
|
||||
}
|
||||
connector.setActiveContact(keyHex);
|
||||
_connector = connector;
|
||||
if (anchor != null) {
|
||||
setState(() => _pendingUnreadScrollTarget = anchor);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
final ctx = _unreadScrollKey.currentContext;
|
||||
if (ctx != null) {
|
||||
Scrollable.ensureVisible(
|
||||
ctx,
|
||||
duration: const Duration(milliseconds: 350),
|
||||
alignment: 0.15,
|
||||
);
|
||||
}
|
||||
setState(() => _pendingUnreadScrollTarget = null);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Message? _findOldestUnreadAnchor(List<Message> messages, int unreadCount) {
|
||||
if (unreadCount <= 0 || messages.isEmpty) return null;
|
||||
var n = 0;
|
||||
Message? oldest;
|
||||
for (final m in messages.reversed) {
|
||||
if (m.isOutgoing || m.isCli) continue;
|
||||
n++;
|
||||
oldest = m;
|
||||
if (n >= unreadCount) break;
|
||||
}
|
||||
return oldest;
|
||||
}
|
||||
|
||||
void _onTextFieldFocusChange() {
|
||||
if (_textFieldFocusNode.hasFocus && mounted) {
|
||||
_scrollController.handleKeyboardOpen();
|
||||
|
|
@ -246,10 +290,79 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
tooltip: context.l10n.chat_pathManagement,
|
||||
onPressed: () => _showPathHistory(context),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.info_outline),
|
||||
onPressed: () => _showContactInfo(context),
|
||||
Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, _) {
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const RadioStatsIconButton(),
|
||||
],
|
||||
),
|
||||
body: Consumer<MeshCoreConnector>(
|
||||
|
|
@ -309,6 +422,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
// Auto-scroll to bottom if user is already at bottom
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
if (_pendingUnreadScrollTarget != null) return;
|
||||
_scrollController.scrollToBottomIfAtBottom();
|
||||
});
|
||||
|
||||
|
|
@ -355,7 +469,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
(service) => service.scale,
|
||||
);
|
||||
final resolvedContact = _resolveContact(connector);
|
||||
return _MessageBubble(
|
||||
final bubble = _MessageBubble(
|
||||
message: message,
|
||||
senderName: resolvedContact.type == advTypeRoom
|
||||
? "${contact.name} [$fourByteHex]"
|
||||
|
|
@ -367,6 +481,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
onRetryReaction: (msg, emoji) =>
|
||||
_sendReaction(msg, contact, emoji),
|
||||
);
|
||||
if (identical(message, _pendingUnreadScrollTarget)) {
|
||||
return KeyedSubtree(key: _unreadScrollKey, child: bubble);
|
||||
}
|
||||
return bubble;
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
@ -492,6 +610,16 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
final text = _textController.text.trim();
|
||||
if (text.isEmpty) return;
|
||||
|
||||
final now = DateTime.now();
|
||||
if (_lastTextSendAt != null &&
|
||||
now.difference(_lastTextSendAt!) < const Duration(seconds: 1)) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(context.l10n.chat_sendCooldown)));
|
||||
return;
|
||||
}
|
||||
_lastTextSendAt = now;
|
||||
|
||||
final maxBytes = maxContactMessageBytes();
|
||||
if (utf8.encode(text).length > maxBytes) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
|
|
@ -881,6 +1009,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
path: Uint8List.fromList(pathBytes),
|
||||
flipPathAround: true,
|
||||
targetContact: widget.contact,
|
||||
pathHashByteWidth: connector.pathHashByteWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -895,11 +1024,22 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
int _resolveContactIndex = -1;
|
||||
|
||||
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,
|
||||
orElse: () => widget.contact,
|
||||
);
|
||||
if (_resolveContactIndex == -1) {
|
||||
return widget.contact;
|
||||
}
|
||||
return connector.contacts[_resolveContactIndex];
|
||||
}
|
||||
|
||||
Contact _resolveContactFrom4Bytes(
|
||||
|
|
@ -952,59 +1092,127 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
|
||||
void _showContactInfo(BuildContext context) {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
connector.ensureContactSmazSettingLoaded(widget.contact.publicKeyHex);
|
||||
|
||||
final contact = _resolveContact(connector);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, _) {
|
||||
final contact = _resolveContact(connector);
|
||||
final smazEnabled = connector.isContactSmazEnabled(
|
||||
contact.publicKeyHex,
|
||||
);
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(contact.name),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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),
|
||||
builder: (context) => AlertDialog(
|
||||
title: SelectableText(contact.name),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow(context.l10n.chat_type, contact.typeLabel),
|
||||
_buildInfoRow(context.l10n.chat_path, contact.pathLabel),
|
||||
_buildInfoRow(
|
||||
context.l10n.contact_lastSeen,
|
||||
_formatContactLastMessage(contact.lastMessageAt),
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -1019,12 +1227,32 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
width: 80,
|
||||
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) {
|
||||
// Check if this is a repeater
|
||||
context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex);
|
||||
|
|
@ -1044,7 +1272,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
connector.getContacts();
|
||||
}
|
||||
|
||||
final pathForInput = currentContact.pathIdList;
|
||||
final pathForInput = currentContact.pathFormattedIdList(
|
||||
connector.pathHashByteWidth,
|
||||
);
|
||||
final currentPathLabel = _currentPathLabel(currentContact);
|
||||
|
||||
// Filter out the current contact from available contacts
|
||||
|
|
@ -1439,11 +1669,6 @@ class _MessageBubble extends StatelessWidget {
|
|||
color: textColor,
|
||||
fontSize: bodyFontSize * textScale,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
color: Colors.green,
|
||||
decoration: TextDecoration.underline,
|
||||
fontSize: bodyFontSize * textScale,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!enableTracing && isOutgoing) ...[
|
||||
|
|
@ -1472,7 +1697,10 @@ class _MessageBubble extends StatelessWidget {
|
|||
child: Text(
|
||||
context.l10n.chat_retryCount(
|
||||
message.retryCount,
|
||||
4,
|
||||
context
|
||||
.read<AppSettingsService>()
|
||||
.settings
|
||||
.maxMessageRetries,
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
|
|
|
|||
250
lib/screens/companion_radio_stats_screen.dart
Normal file
250
lib/screens/companion_radio_stats_screen.dart
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:meshcore_open/connector/meshcore_connector.dart';
|
||||
import 'package:meshcore_open/models/companion_radio_stats.dart';
|
||||
import 'package:meshcore_open/l10n/l10n.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class CompanionRadioStatsScreen extends StatefulWidget {
|
||||
const CompanionRadioStatsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<CompanionRadioStatsScreen> createState() =>
|
||||
_CompanionRadioStatsScreenState();
|
||||
}
|
||||
|
||||
class _CompanionRadioStatsScreenState extends State<CompanionRadioStatsScreen> {
|
||||
final List<double> _noiseHistory = [];
|
||||
static const int _maxSamples = 120;
|
||||
MeshCoreConnector? _connector;
|
||||
DateTime? _lastChartSampleAt;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final c = context.read<MeshCoreConnector>();
|
||||
_connector = c;
|
||||
c.acquireRadioStatsPolling();
|
||||
c.radioStatsNotifier.addListener(_onStatsUpdate);
|
||||
}
|
||||
|
||||
void _onStatsUpdate() {
|
||||
final s = _connector?.radioStatsNotifier.value;
|
||||
if (s == null || !mounted) return;
|
||||
if (_lastChartSampleAt == s.receivedAt) return;
|
||||
_lastChartSampleAt = s.receivedAt;
|
||||
setState(() {
|
||||
_noiseHistory.add(s.noiseFloorDbm.toDouble());
|
||||
while (_noiseHistory.length > _maxSamples) {
|
||||
_noiseHistory.removeAt(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_connector?.radioStatsNotifier.removeListener(_onStatsUpdate);
|
||||
_connector?.releaseRadioStatsPolling();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.radioStats_screenTitle),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Selector<MeshCoreConnector, ({bool connected, bool supported})>(
|
||||
selector: (_, c) => (
|
||||
connected: c.isConnected,
|
||||
supported: c.supportsCompanionRadioStats,
|
||||
),
|
||||
builder: (context, state, _) {
|
||||
if (!state.connected) {
|
||||
return Center(child: Text(l10n.radioStats_notConnected));
|
||||
}
|
||||
if (!state.supported) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(
|
||||
l10n.radioStats_firmwareTooOld,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final tt = Theme.of(context).textTheme;
|
||||
|
||||
return ValueListenableBuilder<CompanionRadioStats?>(
|
||||
valueListenable: connector.radioStatsNotifier,
|
||||
builder: (context, stats, _) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
if (stats != null) ...[
|
||||
Text(
|
||||
l10n.radioStats_noiseFloor(stats.noiseFloorDbm),
|
||||
style: tt.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(l10n.radioStats_lastRssi(stats.lastRssiDbm)),
|
||||
Text(
|
||||
l10n.radioStats_lastSnr(
|
||||
stats.lastSnrDb.toStringAsFixed(1),
|
||||
),
|
||||
),
|
||||
Text(l10n.radioStats_txAir(stats.txAirSecs)),
|
||||
Text(l10n.radioStats_rxAir(stats.rxAirSecs)),
|
||||
const SizedBox(height: 16),
|
||||
] else
|
||||
Text(l10n.radioStats_waiting),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: CustomPaint(
|
||||
painter: _NoiseChartPainter(
|
||||
samples: List<double>.from(_noiseHistory),
|
||||
colorScheme: scheme,
|
||||
textTheme: tt,
|
||||
),
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.radioStats_chartCaption,
|
||||
style: tt.bodySmall?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NoiseChartPainter extends CustomPainter {
|
||||
final List<double> samples;
|
||||
final ColorScheme colorScheme;
|
||||
final TextTheme textTheme;
|
||||
|
||||
_NoiseChartPainter({
|
||||
required this.samples,
|
||||
required this.colorScheme,
|
||||
required this.textTheme,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final bg = Paint()..color = colorScheme.surfaceContainerHighest;
|
||||
final border = Paint()
|
||||
..color = colorScheme.outlineVariant
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1;
|
||||
final grid = Paint()
|
||||
..color = colorScheme.outlineVariant.withValues(alpha: 0.5)
|
||||
..strokeWidth = 1;
|
||||
final line = Paint()
|
||||
..color = colorScheme.primary
|
||||
..strokeWidth = 2
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
final rect = Rect.fromLTWH(0, 0, size.width, size.height);
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(rect, const Radius.circular(8)),
|
||||
bg,
|
||||
);
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(rect, const Radius.circular(8)),
|
||||
border,
|
||||
);
|
||||
|
||||
const padL = 40.0;
|
||||
const padR = 8.0;
|
||||
const padT = 8.0;
|
||||
const padB = 24.0;
|
||||
final chart = Rect.fromLTRB(
|
||||
padL,
|
||||
padT,
|
||||
size.width - padR,
|
||||
size.height - padB,
|
||||
);
|
||||
|
||||
for (var i = 0; i <= 4; i++) {
|
||||
final y = chart.top + (chart.height * i / 4);
|
||||
canvas.drawLine(Offset(chart.left, y), Offset(chart.right, y), grid);
|
||||
}
|
||||
|
||||
if (samples.length < 2) {
|
||||
final tp = TextPainter(
|
||||
text: TextSpan(
|
||||
text: '—',
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
tp.paint(
|
||||
canvas,
|
||||
Offset(chart.left + 4, chart.top + chart.height / 2 - tp.height / 2),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
double minV = samples.reduce((a, b) => a < b ? a : b);
|
||||
double maxV = samples.reduce((a, b) => a > b ? a : b);
|
||||
if ((maxV - minV).abs() < 1) {
|
||||
minV -= 2;
|
||||
maxV += 2;
|
||||
}
|
||||
final span = maxV - minV;
|
||||
|
||||
for (var i = 0; i <= 2; i++) {
|
||||
final v = maxV - span * i / 2;
|
||||
final tp = _yAxisLabel(v);
|
||||
final y = chart.top + (chart.height * i / 2) - tp.height / 2;
|
||||
tp.paint(canvas, Offset(4, y));
|
||||
}
|
||||
|
||||
final path = Path();
|
||||
for (var i = 0; i < samples.length; i++) {
|
||||
final x = chart.left + (chart.width * i / (samples.length - 1));
|
||||
final t = (samples[i] - minV) / span;
|
||||
final y = chart.bottom - t * chart.height;
|
||||
if (i == 0) {
|
||||
path.moveTo(x, y);
|
||||
} else {
|
||||
path.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
canvas.drawPath(path, line);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _NoiseChartPainter oldDelegate) {
|
||||
return oldDelegate.samples.length != samples.length ||
|
||||
oldDelegate.colorScheme != colorScheme;
|
||||
}
|
||||
|
||||
TextPainter _yAxisLabel(double v) {
|
||||
final tp = TextPainter(
|
||||
text: TextSpan(
|
||||
text: v.round().toString(),
|
||||
style: textTheme.labelSmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
return tp;
|
||||
}
|
||||
}
|
||||
|
|
@ -1244,6 +1244,9 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
? Text(context.l10n.contacts_pathTrace)
|
||||
: Text(context.l10n.contacts_ping),
|
||||
onTap: () {
|
||||
final hw = context
|
||||
.read<MeshCoreConnector>()
|
||||
.pathHashByteWidth;
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
|
@ -1254,6 +1257,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
path: contact.pathBytesForDisplay,
|
||||
flipPathAround: true,
|
||||
targetContact: contact,
|
||||
pathHashByteWidth: hw,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -1274,6 +1278,9 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
? Text(context.l10n.contacts_pathTrace)
|
||||
: Text(context.l10n.contacts_ping),
|
||||
onTap: () {
|
||||
final hw = context
|
||||
.read<MeshCoreConnector>()
|
||||
.pathHashByteWidth;
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
|
@ -1284,6 +1291,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
path: contact.pathBytesForDisplay,
|
||||
flipPathAround: contact.pathBytesForDisplay.isNotEmpty,
|
||||
targetContact: contact,
|
||||
pathHashByteWidth: hw,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -1318,6 +1326,9 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
leading: const Icon(Icons.radar, color: Colors.green),
|
||||
title: Text(context.l10n.contacts_chatTraceRoute),
|
||||
onTap: () {
|
||||
final hw = context
|
||||
.read<MeshCoreConnector>()
|
||||
.pathHashByteWidth;
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
|
@ -1328,6 +1339,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
path: contact.pathBytesForDisplay,
|
||||
flipPathAround: true,
|
||||
targetContact: contact,
|
||||
pathHashByteWidth: hw,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -1354,7 +1366,10 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
),
|
||||
onTap: () async {
|
||||
Navigator.pop(sheetContext);
|
||||
await connector.setContactFavorite(contact, !isFavorite);
|
||||
await connector.setContactFlags(
|
||||
contact,
|
||||
isFavorite: !isFavorite,
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
|
|
|
|||
|
|
@ -1,280 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../utils/dialog_utils.dart';
|
||||
import '../utils/disconnect_navigation_mixin.dart';
|
||||
import '../utils/route_transitions.dart';
|
||||
import '../widgets/quick_switch_bar.dart';
|
||||
import 'channels_screen.dart';
|
||||
import 'contacts_screen.dart';
|
||||
import 'map_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
|
||||
/// Main hub screen after connecting to a MeshCore device
|
||||
class DeviceScreen extends StatefulWidget {
|
||||
const DeviceScreen({super.key});
|
||||
|
||||
@override
|
||||
State<DeviceScreen> createState() => _DeviceScreenState();
|
||||
}
|
||||
|
||||
class _DeviceScreenState extends State<DeviceScreen>
|
||||
with DisconnectNavigationMixin {
|
||||
bool _showBatteryVoltage = false;
|
||||
int _quickIndex = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, child) {
|
||||
// Auto-navigate back to scanner if disconnected
|
||||
if (!checkConnectionAndNavigate(connector)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: _buildBatteryIndicator(connector, context),
|
||||
titleSpacing: 16,
|
||||
centerTitle: false,
|
||||
title: _buildAppBarTitle(connector, theme),
|
||||
automaticallyImplyLeading: false,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.bluetooth_disabled),
|
||||
tooltip: context.l10n.common_disconnect,
|
||||
onPressed: () => _disconnect(context, connector),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.tune),
|
||||
tooltip: context.l10n.common_settings,
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const SettingsScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
|
||||
children: [
|
||||
_buildConnectionCard(connector, context),
|
||||
const SizedBox(height: 16),
|
||||
_buildSectionLabel(theme, context.l10n.device_quickSwitch),
|
||||
const SizedBox(height: 12),
|
||||
_buildQuickSwitchBar(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAppBarTitle(MeshCoreConnector connector, ThemeData theme) {
|
||||
final colorScheme = theme.colorScheme;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.device_meshcore,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.8,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
connector.deviceDisplayName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionLabel(ThemeData theme, String text) {
|
||||
return Text(
|
||||
text,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.6,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildConnectionCard(
|
||||
MeshCoreConnector connector,
|
||||
BuildContext context,
|
||||
) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundColor: colorScheme.primaryContainer,
|
||||
child: Icon(
|
||||
Icons.wifi_tethering_rounded,
|
||||
color: colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
connector.deviceDisplayName,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
connector.deviceIdLabel,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
Chip(
|
||||
avatar: Icon(
|
||||
Icons.check_circle,
|
||||
size: 18,
|
||||
color: colorScheme.onSecondaryContainer,
|
||||
),
|
||||
label: Text(context.l10n.common_connected),
|
||||
backgroundColor: colorScheme.secondaryContainer,
|
||||
labelStyle: theme.textTheme.labelMedium?.copyWith(
|
||||
color: colorScheme.onSecondaryContainer,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
_buildBatteryIndicator(connector, context),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickSwitchBar(BuildContext context) {
|
||||
return QuickSwitchBar(
|
||||
selectedIndex: _quickIndex,
|
||||
onDestinationSelected: (index) {
|
||||
_openQuickDestination(index, context);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBatteryIndicator(
|
||||
MeshCoreConnector connector,
|
||||
BuildContext context,
|
||||
) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final percent = connector.batteryPercent;
|
||||
final millivolts = connector.batteryMillivolts;
|
||||
final percentLabel = percent != null ? '$percent%' : '--%';
|
||||
final voltageLabel = millivolts == null
|
||||
? '-- V'
|
||||
: '${(millivolts / 1000.0).toStringAsFixed(2)} V';
|
||||
final displayLabel = _showBatteryVoltage ? voltageLabel : percentLabel;
|
||||
final icon = _batteryIcon(percent);
|
||||
|
||||
return ActionChip(
|
||||
avatar: Icon(icon, size: 16, color: colorScheme.onSecondaryContainer),
|
||||
label: Text(displayLabel),
|
||||
labelStyle: theme.textTheme.labelMedium?.copyWith(
|
||||
color: colorScheme.onSecondaryContainer,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
backgroundColor: colorScheme.secondaryContainer,
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_showBatteryVoltage = !_showBatteryVoltage;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
IconData _batteryIcon(int? percent) {
|
||||
if (percent == null) return Icons.battery_unknown;
|
||||
if (percent <= 15) return Icons.battery_alert;
|
||||
return Icons.battery_full;
|
||||
}
|
||||
|
||||
void _openQuickDestination(int index, BuildContext context) {
|
||||
if (_quickIndex != index) {
|
||||
setState(() {
|
||||
_quickIndex = index;
|
||||
});
|
||||
}
|
||||
switch (index) {
|
||||
case 0:
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
buildQuickSwitchRoute(const ContactsScreen(hideBackButton: true)),
|
||||
);
|
||||
break;
|
||||
case 1:
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
buildQuickSwitchRoute(const ChannelsScreen(hideBackButton: true)),
|
||||
);
|
||||
break;
|
||||
case 2:
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
buildQuickSwitchRoute(const MapScreen(hideBackButton: true)),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _disconnect(
|
||||
BuildContext context,
|
||||
MeshCoreConnector connector,
|
||||
) async {
|
||||
await showDisconnectDialog(context, connector);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:collection';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
|
|
@ -52,7 +53,7 @@ class MapScreen extends StatefulWidget {
|
|||
|
||||
class _MapScreenState extends State<MapScreen> {
|
||||
// Zoom level at which node labels start to appear
|
||||
static const double _labelZoomThreshold = 12.0;
|
||||
static const double _labelZoomThreshold = 14.0;
|
||||
|
||||
final MapController _mapController = MapController();
|
||||
final MapMarkerService _markerService = MapMarkerService();
|
||||
|
|
@ -329,7 +330,9 @@ class _MapScreenState extends State<MapScreen> {
|
|||
if (!_isBuildingPathTrace)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.radar),
|
||||
onPressed: () => _startPath(),
|
||||
onPressed: () => _startPath(
|
||||
LatLng(connector.selfLatitude!, connector.selfLongitude!),
|
||||
),
|
||||
tooltip: context.l10n.contacts_pathTrace,
|
||||
),
|
||||
if (!_isBuildingPathTrace)
|
||||
|
|
@ -477,10 +480,12 @@ class _MapScreenState extends State<MapScreen> {
|
|||
point: highlightPosition,
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Icon(
|
||||
Icons.location_on_outlined,
|
||||
color: Colors.red[600],
|
||||
size: 34,
|
||||
child: IgnorePointer(
|
||||
child: Icon(
|
||||
Icons.location_on_outlined,
|
||||
color: Colors.red[600],
|
||||
size: 34,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!_isBuildingPathTrace)
|
||||
|
|
@ -503,28 +508,33 @@ class _MapScreenState extends State<MapScreen> {
|
|||
),
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.teal,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Colors.white,
|
||||
width: 2,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
child: IgnorePointer(
|
||||
ignoring: true,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.teal,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Colors.white,
|
||||
width: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const Icon(
|
||||
Icons.person_pin_circle,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(
|
||||
alpha: 0.3,
|
||||
),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const Icon(
|
||||
Icons.person_pin_circle,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -544,6 +554,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||
),
|
||||
if (!_isBuildingPathTrace)
|
||||
_buildLegend(
|
||||
contacts,
|
||||
contactsWithLocation,
|
||||
settings,
|
||||
sharedMarkers.length,
|
||||
|
|
@ -580,6 +591,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||
// Index known-location repeaters by their 1-byte hash.
|
||||
// null value = two repeaters share the same hash byte (ambiguous collision).
|
||||
final repeaterByHash = <int, Contact?>{};
|
||||
|
||||
for (final c in withLocation) {
|
||||
if (c.type == advTypeRepeater) {
|
||||
if (repeaterByHash.containsKey(c.publicKey[0])) {
|
||||
|
|
@ -595,6 +607,11 @@ class _MapScreenState extends State<MapScreen> {
|
|||
|
||||
for (final contact in allContacts) {
|
||||
if (contact.hasLocation) continue;
|
||||
if (contact.lastSeen.isBefore(
|
||||
DateTime.now().subtract(const Duration(hours: 24)),
|
||||
)) {
|
||||
continue; // skip stale contacts
|
||||
}
|
||||
|
||||
final anchorSet = <LatLng>{};
|
||||
|
||||
|
|
@ -641,10 +658,19 @@ class _MapScreenState extends State<MapScreen> {
|
|||
continue; // discard implausible guesses near (0, 0)
|
||||
}
|
||||
} else {
|
||||
double lat = 0, lon = 0;
|
||||
double lat = 0, lon = 0, weight = 1.0;
|
||||
int counted = 0;
|
||||
for (final a in anchors) {
|
||||
lat += a.latitude;
|
||||
lon += a.longitude;
|
||||
if (counted == 0) {
|
||||
lat = a.latitude;
|
||||
lon = a.longitude;
|
||||
} else {
|
||||
lat += a.latitude * weight;
|
||||
lon += a.longitude * weight;
|
||||
}
|
||||
// weight subsequent anchors less to create a bias towards the first (if more than 2)
|
||||
weight = weight / 2;
|
||||
counted++;
|
||||
}
|
||||
position = _offsetGuessedPosition(
|
||||
LatLng(lat / anchors.length, lon / anchors.length),
|
||||
|
|
@ -812,31 +838,70 @@ class _MapScreenState extends State<MapScreen> {
|
|||
return markers;
|
||||
}
|
||||
|
||||
List<Contact> _filterContactsBySettings(
|
||||
List<Contact> contacts,
|
||||
dynamic settings, {
|
||||
bool noLocations = false,
|
||||
}) {
|
||||
List<Contact> filtered = [];
|
||||
bool addContact = false;
|
||||
for (final contact in contacts) {
|
||||
addContact = false;
|
||||
if (!contact.hasLocation && !noLocations) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply node type filters
|
||||
if (contact.type == advTypeRepeater &&
|
||||
(settings.mapShowRepeaters ||
|
||||
_isBuildingPathTrace ||
|
||||
settings.mapShowOverlaps)) {
|
||||
addContact = true;
|
||||
}
|
||||
if (contact.type == advTypeChat &&
|
||||
(settings.mapShowChatNodes || _isBuildingPathTrace)) {
|
||||
addContact = true;
|
||||
}
|
||||
if (contact.type != advTypeChat &&
|
||||
contact.type != advTypeRepeater &&
|
||||
(settings.mapShowOtherNodes ||
|
||||
_isBuildingPathTrace ||
|
||||
settings.mapShowOverlaps)) {
|
||||
addContact = true;
|
||||
}
|
||||
|
||||
final hasOverlap = contacts
|
||||
.where(
|
||||
(c) =>
|
||||
c.publicKeyHex != contact.publicKeyHex &&
|
||||
c.publicKey.first == contact.publicKey.first &&
|
||||
(c.type == advTypeRepeater || c.type == advTypeRoom) &&
|
||||
(contact.type == advTypeRepeater ||
|
||||
contact.type == advTypeRoom),
|
||||
)
|
||||
.firstOrNull;
|
||||
|
||||
if (hasOverlap == null &&
|
||||
settings.mapShowOverlaps &&
|
||||
!_isBuildingPathTrace) {
|
||||
addContact = false;
|
||||
}
|
||||
|
||||
if (addContact) {
|
||||
filtered.add(contact);
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
List<Marker> _buildMarkers(
|
||||
List<Contact> contacts,
|
||||
settings, {
|
||||
required bool showLabels,
|
||||
}) {
|
||||
final markers = <Marker>[];
|
||||
|
||||
for (final contact in contacts) {
|
||||
if (!contact.hasLocation) continue;
|
||||
|
||||
// Apply node type filters
|
||||
if (contact.type == advTypeRepeater &&
|
||||
(!settings.mapShowRepeaters && !_isBuildingPathTrace)) {
|
||||
continue;
|
||||
}
|
||||
if (contact.type == advTypeChat &&
|
||||
!(settings.mapShowChatNodes && !_isBuildingPathTrace)) {
|
||||
continue;
|
||||
}
|
||||
if (contact.type != advTypeChat &&
|
||||
contact.type != advTypeRepeater &&
|
||||
(!settings.mapShowOtherNodes && !_isBuildingPathTrace)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final filteredContacts = _filterContactsBySettings(contacts, settings);
|
||||
for (final contact in filteredContacts) {
|
||||
final marker = Marker(
|
||||
point: LatLng(contact.latitude!, contact.longitude!),
|
||||
width: 35,
|
||||
|
|
@ -852,7 +917,9 @@ class _MapScreenState extends State<MapScreen> {
|
|||
Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: _getNodeColor(contact.type),
|
||||
color: settings.mapShowOverlaps && !_isBuildingPathTrace
|
||||
? Colors.red
|
||||
: _getNodeColor(contact.type),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
|
|
@ -879,7 +946,9 @@ class _MapScreenState extends State<MapScreen> {
|
|||
markers.add(
|
||||
_buildNodeLabelMarker(
|
||||
point: LatLng(contact.latitude!, contact.longitude!),
|
||||
label: contact.name,
|
||||
label: settings.mapShowOverlaps && !_isBuildingPathTrace
|
||||
? "${contact.publicKeyHex.substring(0, 2)}:${contact.name}"
|
||||
: contact.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -954,25 +1023,25 @@ class _MapScreenState extends State<MapScreen> {
|
|||
}
|
||||
|
||||
Widget _buildLegend(
|
||||
List<Contact> contacts,
|
||||
List<Contact> contactsWithLocation,
|
||||
settings,
|
||||
int markerCount,
|
||||
int guessedCount,
|
||||
) {
|
||||
int nodeCount = 0;
|
||||
for (final contact in contactsWithLocation) {
|
||||
// Apply node type filters
|
||||
if (contact.type == advTypeRepeater && !settings.mapShowRepeaters) {
|
||||
continue;
|
||||
}
|
||||
if (contact.type == advTypeChat && !settings.mapShowChatNodes) continue;
|
||||
if (contact.type != advTypeChat &&
|
||||
contact.type != advTypeRepeater &&
|
||||
!settings.mapShowOtherNodes) {
|
||||
continue;
|
||||
}
|
||||
nodeCount++;
|
||||
}
|
||||
final filteredContacts = _filterContactsBySettings(
|
||||
contacts,
|
||||
settings,
|
||||
noLocations: false,
|
||||
);
|
||||
final filteredContactsAll = _filterContactsBySettings(
|
||||
contacts,
|
||||
settings,
|
||||
noLocations: true,
|
||||
);
|
||||
|
||||
final nodeCount = filteredContacts.length;
|
||||
final nodeCountAll = filteredContactsAll.length;
|
||||
|
||||
return Positioned(
|
||||
top: 16,
|
||||
|
|
@ -1008,6 +1077,54 @@ class _MapScreenState extends State<MapScreen> {
|
|||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.location_on,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
Text(
|
||||
": $nodeCount",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.wrong_location,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
Text(
|
||||
": ${nodeCountAll - nodeCount}",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.add_outlined,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
Text(
|
||||
": $nodeCountAll",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
context.l10n.map_pinsCount(markerCount),
|
||||
style: const TextStyle(
|
||||
|
|
@ -1846,6 +1963,15 @@ class _MapScreenState extends State<MapScreen> {
|
|||
},
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
CheckboxListTile(
|
||||
title: Text(context.l10n.map_showOverlaps),
|
||||
value: settings.mapShowOverlaps,
|
||||
onChanged: (value) {
|
||||
service.setMapShowOverlaps(value ?? true);
|
||||
},
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
context.l10n.map_keyPrefix,
|
||||
|
|
@ -2004,12 +2130,13 @@ class _MapScreenState extends State<MapScreen> {
|
|||
});
|
||||
}
|
||||
|
||||
void _startPath() {
|
||||
void _startPath(LatLng position) {
|
||||
setState(() {
|
||||
_isBuildingPathTrace = true;
|
||||
_pathTrace.clear();
|
||||
_points.clear();
|
||||
_polylines.clear();
|
||||
_points.add(position);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -2055,21 +2182,25 @@ class _MapScreenState extends State<MapScreen> {
|
|||
.join(','),
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
// const SizedBox(height: 6),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
spacing: 1,
|
||||
runSpacing: 1,
|
||||
children: [
|
||||
if (_pathTrace.isNotEmpty)
|
||||
ElevatedButton(
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
final hashW = context
|
||||
.read<MeshCoreConnector>()
|
||||
.pathHashByteWidth;
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PathTraceMapScreen(
|
||||
title: l10n.contacts_pathTrace,
|
||||
path: Uint8List.fromList(_pathTrace),
|
||||
pathHashByteWidth: hashW,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -2077,15 +2208,37 @@ class _MapScreenState extends State<MapScreen> {
|
|||
_isBuildingPathTrace = false;
|
||||
});
|
||||
},
|
||||
child: Text(l10n.map_runTrace),
|
||||
tooltip: l10n.map_runTrace,
|
||||
icon: const Icon(Icons.arrow_forward_outlined),
|
||||
),
|
||||
if (_pathTrace.isNotEmpty)
|
||||
ElevatedButton(
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PathTraceMapScreen(
|
||||
title: l10n.contacts_pathTrace,
|
||||
path: Uint8List.fromList(_pathTrace),
|
||||
flipPathAround: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_isBuildingPathTrace = false;
|
||||
});
|
||||
},
|
||||
tooltip: l10n.map_runTraceWithReturnPath,
|
||||
icon: const Icon(Icons.replay),
|
||||
),
|
||||
if (_pathTrace.isNotEmpty)
|
||||
IconButton(
|
||||
onPressed: _removePath,
|
||||
child: Text(l10n.map_removeLast),
|
||||
tooltip: l10n.map_removeLast,
|
||||
icon: const Icon(Icons.undo),
|
||||
),
|
||||
if (_pathTrace.isEmpty)
|
||||
ElevatedButton(
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isBuildingPathTrace = false;
|
||||
|
|
@ -2097,7 +2250,8 @@ class _MapScreenState extends State<MapScreen> {
|
|||
SnackBar(content: Text(l10n.map_pathTraceCancelled)),
|
||||
);
|
||||
},
|
||||
child: Text(l10n.common_cancel),
|
||||
tooltip: l10n.common_cancel,
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -44,6 +44,24 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
|||
PathSelection? _pendingStatusSelection;
|
||||
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
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -163,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 {
|
||||
if (_commandService == null) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ class PathTraceMapScreen extends StatefulWidget {
|
|||
final bool flipPathAround;
|
||||
final bool reversePathAround;
|
||||
final Contact? targetContact;
|
||||
final int pathHashByteWidth;
|
||||
|
||||
const PathTraceMapScreen({
|
||||
super.key,
|
||||
|
|
@ -64,6 +65,7 @@ class PathTraceMapScreen extends StatefulWidget {
|
|||
this.flipPathAround = false,
|
||||
this.reversePathAround = false,
|
||||
this.targetContact,
|
||||
this.pathHashByteWidth = pathHashSize,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -119,8 +121,13 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||
Uint8List traceBytes;
|
||||
|
||||
if (pathBytes.isEmpty) {
|
||||
final pk = widget.targetContact?.publicKey;
|
||||
final n = widget.pathHashByteWidth.clamp(1, pubKeySize);
|
||||
if (pk != null && pk.length >= n) {
|
||||
return Uint8List.fromList(pk.sublist(0, n));
|
||||
}
|
||||
traceBytes = Uint8List(1);
|
||||
traceBytes[0] = widget.targetContact?.publicKey[0] ?? 0;
|
||||
traceBytes[0] = pk?[0] ?? 0;
|
||||
return traceBytes;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -77,11 +77,22 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
|||
});
|
||||
}
|
||||
|
||||
int _resolveRepeaterIndex = -1;
|
||||
|
||||
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,
|
||||
orElse: () => widget.repeater,
|
||||
);
|
||||
if (_resolveRepeaterIndex == -1) {
|
||||
return widget.repeater;
|
||||
}
|
||||
return connector.contacts[_resolveRepeaterIndex];
|
||||
}
|
||||
|
||||
void _handleTextMessageResponse(Uint8List frame) {
|
||||
|
|
|
|||
|
|
@ -205,8 +205,7 @@ class RepeaterHubScreen extends StatelessWidget {
|
|||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
TelemetryScreen(repeater: repeater, password: password),
|
||||
builder: (context) => TelemetryScreen(contact: repeater),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -129,11 +129,22 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||
_commandService?.handleResponse(widget.repeater, parsed.text);
|
||||
}
|
||||
|
||||
int _resolveRepeaterIndex = -1;
|
||||
|
||||
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,
|
||||
orElse: () => widget.repeater,
|
||||
);
|
||||
if (_resolveRepeaterIndex == -1) {
|
||||
return widget.repeater;
|
||||
}
|
||||
return connector.contacts[_resolveRepeaterIndex];
|
||||
}
|
||||
|
||||
bool _matchesRepeaterPrefix(Uint8List prefix) {
|
||||
|
|
|
|||
|
|
@ -91,11 +91,22 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
|||
});
|
||||
}
|
||||
|
||||
int _resolveRepeaterIndex = -1;
|
||||
|
||||
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,
|
||||
orElse: () => widget.repeater,
|
||||
);
|
||||
if (_resolveRepeaterIndex == -1) {
|
||||
return widget.repeater;
|
||||
}
|
||||
return connector.contacts[_resolveRepeaterIndex];
|
||||
}
|
||||
|
||||
void _handleTextMessageResponse(Uint8List frame) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import '../widgets/app_bar.dart';
|
|||
import 'app_settings_screen.dart';
|
||||
import 'app_debug_log_screen.dart';
|
||||
import 'ble_debug_log_screen.dart';
|
||||
import '../widgets/radio_stats_entry.dart';
|
||||
|
||||
class SettingsScreen extends StatefulWidget {
|
||||
const SettingsScreen({super.key});
|
||||
|
|
@ -269,6 +270,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
onTap: () => _showRadioSettings(context, connector),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.sensors_outlined),
|
||||
title: Text(l10n.radioStats_settingsTile),
|
||||
subtitle: Text(l10n.radioStats_settingsSubtitle),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
enabled:
|
||||
connector.isConnected && connector.supportsCompanionRadioStats,
|
||||
onTap: () => pushCompanionRadioStatsScreen(context),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.location_on_outlined),
|
||||
title: Text(l10n.settings_location),
|
||||
|
|
@ -287,10 +298,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.visibility_off_outlined),
|
||||
title: Text(l10n.settings_privacyMode),
|
||||
subtitle: Text(l10n.settings_privacyModeSubtitle),
|
||||
title: Text(l10n.settings_privacy),
|
||||
subtitle: Text(l10n.settings_privacySubtitle),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _togglePrivacy(context, connector),
|
||||
onTap: () => _privacySettings(context, connector),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -311,10 +322,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.cell_tower),
|
||||
title: Text(l10n.settings_sendAdvertisement),
|
||||
subtitle: Text(l10n.settings_sendAdvertisementSubtitle),
|
||||
onTap: () => _sendAdvert(context, connector),
|
||||
leading: const Icon(Icons.delete_outline, color: Colors.red),
|
||||
title: Text("Delete All Paths"),
|
||||
subtitle: Text(
|
||||
"Clear all path data from contacts.",
|
||||
style: TextStyle(color: Colors.red[700]),
|
||||
),
|
||||
onTap: () => connector.deleteAllPaths(),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
|
|
@ -657,55 +671,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) {
|
||||
final l10n = context.l10n;
|
||||
connector.sendSelfAdvert(flood: true);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.settings_advertisementSent)));
|
||||
}
|
||||
|
||||
void _syncTime(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
connector.syncTime();
|
||||
|
|
@ -977,6 +942,136 @@ 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);
|
||||
},
|
||||
),
|
||||
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 {
|
||||
final MeshCoreConnector connector;
|
||||
|
||||
|
|
|
|||
|
|
@ -10,30 +10,22 @@ import '../connector/meshcore_connector.dart';
|
|||
import '../connector/meshcore_protocol.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/repeater_command_service.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../widgets/path_management_dialog.dart';
|
||||
import '../helpers/cayenne_lpp.dart';
|
||||
import '../utils/battery_utils.dart';
|
||||
|
||||
class TelemetryScreen extends StatefulWidget {
|
||||
final Contact repeater;
|
||||
final String password;
|
||||
final Contact contact;
|
||||
|
||||
const TelemetryScreen({
|
||||
super.key,
|
||||
required this.repeater,
|
||||
required this.password,
|
||||
});
|
||||
const TelemetryScreen({super.key, required this.contact});
|
||||
|
||||
@override
|
||||
State<TelemetryScreen> createState() => _TelemetryScreenState();
|
||||
}
|
||||
|
||||
class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
static const int _statusPayloadOffset = 8;
|
||||
static const int _statusStatsSize = 52;
|
||||
static const int _statusResponseBytes =
|
||||
_statusPayloadOffset + _statusStatsSize;
|
||||
Uint8List _tagData = Uint8List(4);
|
||||
int _tagData = 0;
|
||||
|
||||
bool _isLoading = false;
|
||||
bool _isLoaded = false;
|
||||
|
|
@ -44,6 +36,26 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||
PathSelection? _pendingStatusSelection;
|
||||
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
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -60,27 +72,62 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||
// Listen for incoming text messages from the repeater
|
||||
_frameSubscription = connector.receivedFrames.listen((frame) {
|
||||
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) {
|
||||
_tagData = frame.sublist(2, 6);
|
||||
}
|
||||
// Check if it's a binary response
|
||||
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
|
||||
if (frame[0] == pushCodeBinaryResponse &&
|
||||
listEquals(frame.sublist(2, 6), _tagData)) {
|
||||
if (!mounted) return;
|
||||
_handleStatusResponse(frame.sublist(6));
|
||||
// Check if it's a telemetry response (for chat contacts)
|
||||
if (cmd == pushCodeTelemetryResponse) {
|
||||
reader.skipBytes(1); // Skip the reserved byte
|
||||
final pubkey = reader.readBytes(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 batteryMv = _extractTelemetryBatteryMillivolts(parsedTelemetry);
|
||||
if (batteryMv != null) {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
connector.updateRepeaterBatterySnapshot(
|
||||
widget.repeater.publicKeyHex,
|
||||
widget.contact.publicKeyHex,
|
||||
batteryMv,
|
||||
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 {
|
||||
if (_commandService == null) return;
|
||||
|
||||
|
|
@ -121,41 +161,20 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||
});
|
||||
try {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
final repeater = _resolveRepeater(connector);
|
||||
final selection = await connector.preparePathForContactSend(repeater);
|
||||
final selection = await connector.preparePathForContactSend(
|
||||
_resolveContact(connector),
|
||||
);
|
||||
_pendingStatusSelection = selection;
|
||||
final frame = buildSendBinaryReq(
|
||||
repeater.publicKey,
|
||||
payload: Uint8List.fromList([reqTypeGetTelemetry]),
|
||||
);
|
||||
await connector.sendFrame(frame);
|
||||
|
||||
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,
|
||||
),
|
||||
Uint8List frame;
|
||||
if (widget.contact.type != advTypeChat) {
|
||||
frame = buildSendBinaryReq(
|
||||
widget.contact.publicKey,
|
||||
payload: Uint8List.fromList([reqTypeGetTelemetry]),
|
||||
);
|
||||
_recordStatusResult(false);
|
||||
});
|
||||
} else {
|
||||
frame = buildSendTelemetryReq(widget.contact.publicKey);
|
||||
}
|
||||
await connector.sendFrame(frame);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
|
|
@ -173,12 +192,16 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
void _recordStatusResult(bool success) {
|
||||
void _recordTelemetryResult(bool success) {
|
||||
final selection = _pendingStatusSelection;
|
||||
if (selection == null) return;
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
final repeater = _resolveRepeater(connector);
|
||||
connector.recordRepeaterPathResult(repeater, selection, success, null);
|
||||
connector.recordRepeaterPathResult(
|
||||
widget.contact,
|
||||
selection,
|
||||
success,
|
||||
null,
|
||||
);
|
||||
_pendingStatusSelection = null;
|
||||
}
|
||||
|
||||
|
|
@ -196,8 +219,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||
final connector = context.watch<MeshCoreConnector>();
|
||||
final settings = context.watch<AppSettingsService>().settings;
|
||||
final isImperialUnits = settings.unitSystem == UnitSystem.imperial;
|
||||
final repeater = _resolveRepeater(connector);
|
||||
final isFloodMode = repeater.pathOverride == -1;
|
||||
final isFloodMode = widget.contact.pathOverride == -1;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
|
|
@ -210,7 +232,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
repeater.name,
|
||||
widget.contact.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.normal,
|
||||
|
|
@ -225,9 +247,9 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||
tooltip: l10n.repeater_routingMode,
|
||||
onSelected: (mode) async {
|
||||
if (mode == 'flood') {
|
||||
await connector.setPathOverride(repeater, pathLen: -1);
|
||||
await connector.setPathOverride(widget.contact, pathLen: -1);
|
||||
} else {
|
||||
await connector.setPathOverride(repeater, pathLen: null);
|
||||
await connector.setPathOverride(widget.contact, pathLen: null);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
|
|
@ -283,7 +305,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||
icon: const Icon(Icons.timeline),
|
||||
tooltip: l10n.repeater_pathManagement,
|
||||
onPressed: () =>
|
||||
PathManagementDialog.show(context, contact: repeater),
|
||||
PathManagementDialog.show(context, contact: widget.contact),
|
||||
),
|
||||
IconButton(
|
||||
icon: _isLoading
|
||||
|
|
@ -437,7 +459,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||
final l10n = context.l10n;
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
final batteryMv =
|
||||
connector.getRepeaterBatteryMillivolts(widget.repeater.publicKeyHex) ??
|
||||
connector.getRepeaterBatteryMillivolts(widget.contact.publicKeyHex) ??
|
||||
(telemetryVolts == null ? null : (telemetryVolts * 1000).round());
|
||||
if (batteryMv == null) return l10n.common_notAvailable;
|
||||
final chemistry = _batteryChemistry();
|
||||
|
|
@ -449,7 +471,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||
String _batteryChemistry() {
|
||||
final settingsService = context.read<AppSettingsService>();
|
||||
return settingsService.batteryChemistryForRepeater(
|
||||
widget.repeater.publicKeyHex,
|
||||
widget.contact.publicKeyHex,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,6 +64,10 @@ class AppSettingsService extends ChangeNotifier {
|
|||
await updateSettings(_settings.copyWith(mapShowOtherNodes: value));
|
||||
}
|
||||
|
||||
Future<void> setMapShowOverlaps(bool value) async {
|
||||
await updateSettings(_settings.copyWith(mapShowOverlaps: value));
|
||||
}
|
||||
|
||||
Future<void> setMapTimeFilterHours(double value) async {
|
||||
await updateSettings(_settings.copyWith(mapTimeFilterHours: value));
|
||||
}
|
||||
|
|
@ -214,4 +218,8 @@ class AppSettingsService extends ChangeNotifier {
|
|||
Future<void> setTcpServerPort(int value) async {
|
||||
await updateSettings(_settings.copyWith(tcpServerPort: value));
|
||||
}
|
||||
|
||||
Future<void> setJumpToOldestUnread(bool value) async {
|
||||
await updateSettings(_settings.copyWith(jumpToOldestUnread: value));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import 'app_debug_log_service.dart';
|
|||
|
||||
class _AckHistoryEntry {
|
||||
final String messageId;
|
||||
final List<Uint8List> ackHashes;
|
||||
final List<int> ackHashes;
|
||||
final DateTime timestamp;
|
||||
|
||||
_AckHistoryEntry({
|
||||
|
|
@ -21,11 +21,16 @@ class _AckHistoryEntry {
|
|||
});
|
||||
}
|
||||
|
||||
/// (messageId, timestamp, attemptIndex) — stored per ACK hash for O(1) lookup.
|
||||
/// (messageId, timestamp, attemptIndex, pathSelection) — stored per ACK hash
|
||||
/// for O(1) lookup. [pathSelection] snapshots the route used for this
|
||||
/// specific attempt so that a late PUSH_CODE_SEND_CONFIRMED credits the
|
||||
/// correct path even when the message has since been retried on a different
|
||||
/// route.
|
||||
typedef AckHashMapping = ({
|
||||
String messageId,
|
||||
DateTime timestamp,
|
||||
int attemptIndex,
|
||||
PathSelection? pathSelection,
|
||||
});
|
||||
|
||||
class RetryServiceConfig {
|
||||
|
|
@ -77,7 +82,7 @@ class MessageRetryService extends ChangeNotifier {
|
|||
final Map<String, Contact> _pendingContacts = {};
|
||||
final Map<String, List<PathSelection>> _attemptPathHistory = {};
|
||||
final Map<String, AckHashMapping> _ackHashToMessageId = {};
|
||||
final Map<String, List<Uint8List>> _expectedAckHashes = {};
|
||||
final Map<String, List<int>> _expectedAckHashes = {};
|
||||
final List<_AckHistoryEntry> _ackHistory = [];
|
||||
final Map<String, List<String>> _sendQueue = {};
|
||||
final Set<String> _activeMessages = {};
|
||||
|
|
@ -98,7 +103,7 @@ class MessageRetryService extends ChangeNotifier {
|
|||
|
||||
/// Compute expected ACK hash using same algorithm as firmware:
|
||||
/// SHA256([timestamp(4)][attempt(1)][text][sender_pubkey(32)]) -> first 4 bytes
|
||||
static Uint8List computeExpectedAckHash(
|
||||
static int computeExpectedAckHash(
|
||||
int timestampSeconds,
|
||||
int attempt,
|
||||
String text,
|
||||
|
|
@ -126,7 +131,8 @@ class MessageRetryService extends ChangeNotifier {
|
|||
|
||||
// Compute SHA256 and return first 4 bytes
|
||||
final hash = sha256.convert(buffer);
|
||||
return Uint8List.fromList(hash.bytes.sublist(0, 4));
|
||||
final bytes = Uint8List.fromList(hash.bytes.sublist(0, 4));
|
||||
return (bytes[3] << 24) | (bytes[2] << 16) | (bytes[1] << 8) | bytes[0];
|
||||
}
|
||||
|
||||
Future<void> sendMessageWithRetry({
|
||||
|
|
@ -324,9 +330,7 @@ class MessageRetryService extends ChangeNotifier {
|
|||
outboundText,
|
||||
selfPubKey,
|
||||
);
|
||||
final expectedHashHex = expectedHash
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
final expectedHashHex = expectedHash.toRadixString(16).padLeft(8, '0');
|
||||
_expectedHashToMessageId[expectedHashHex] = messageId;
|
||||
|
||||
final shortText = message.text.length > 20
|
||||
|
|
@ -341,13 +345,11 @@ class MessageRetryService extends ChangeNotifier {
|
|||
config.sendMessage(contact, message.text, attempt, timestampSeconds);
|
||||
}
|
||||
|
||||
bool updateMessageFromSent(Uint8List ackHash, int timeoutMs) {
|
||||
bool updateMessageFromSent(int ackHash, int timeoutMs) {
|
||||
final config = _config;
|
||||
if (config == null) return false;
|
||||
|
||||
final ackHashHex = ackHash
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
final ackHashHex = ackHash.toRadixString(16).padLeft(8, '0');
|
||||
|
||||
// Try hash-based matching (fixes LoRa message drops causing mismatches)
|
||||
String? messageId = _expectedHashToMessageId.remove(ackHashHex);
|
||||
|
|
@ -385,14 +387,13 @@ class MessageRetryService extends ChangeNotifier {
|
|||
messageId: messageId,
|
||||
timestamp: DateTime.now(),
|
||||
attemptIndex: message.retryCount,
|
||||
pathSelection: _selectionFromMessage(message),
|
||||
);
|
||||
|
||||
// Add this ACK hash to the list of expected ACKs for this message (for history)
|
||||
_expectedAckHashes[messageId] ??= [];
|
||||
if (!_expectedAckHashes[messageId]!.any(
|
||||
(hash) => listEquals(hash, ackHash),
|
||||
)) {
|
||||
_expectedAckHashes[messageId]!.add(Uint8List.fromList(ackHash));
|
||||
if (!_expectedAckHashes[messageId]!.any((hash) => hash == ackHash)) {
|
||||
_expectedAckHashes[messageId]!.add(ackHash);
|
||||
}
|
||||
|
||||
// Calculate timeout: prefer ML prediction, then device-provided, then physics fallback
|
||||
|
|
@ -400,14 +401,11 @@ class MessageRetryService extends ChangeNotifier {
|
|||
|
||||
int actualTimeout = timeoutMs;
|
||||
if (config.calculateTimeout != null) {
|
||||
final calculated = config.calculateTimeout!(
|
||||
actualTimeout = config.calculateTimeout!(
|
||||
pathLengthValue,
|
||||
message.text.length,
|
||||
contactKey: contact.publicKeyHex,
|
||||
);
|
||||
if (timeoutMs <= 0 || calculated < timeoutMs) {
|
||||
actualTimeout = calculated;
|
||||
}
|
||||
}
|
||||
|
||||
final updatedMessage = message.copyWith(
|
||||
|
|
@ -559,10 +557,10 @@ class MessageRetryService extends ChangeNotifier {
|
|||
}
|
||||
}
|
||||
|
||||
bool _checkAckHistory(Uint8List ackHash) {
|
||||
bool _checkAckHistory(int ackHash) {
|
||||
for (final entry in _ackHistory) {
|
||||
for (final expectedHash in entry.ackHashes) {
|
||||
if (listEquals(expectedHash, ackHash)) {
|
||||
if (expectedHash == ackHash) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -570,13 +568,12 @@ class MessageRetryService extends ChangeNotifier {
|
|||
return false;
|
||||
}
|
||||
|
||||
void handleAckReceived(Uint8List ackHash, int tripTimeMs) {
|
||||
void handleAckReceived(int ackHash, int tripTimeMs) {
|
||||
final config = _config;
|
||||
String? matchedMessageId;
|
||||
int? matchedAttemptIndex;
|
||||
final ackHashHex = ackHash
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
PathSelection? matchedPathSelection;
|
||||
final ackHashHex = ackHash.toRadixString(16).padLeft(8, '0');
|
||||
|
||||
// Clean up old ACK hash mappings (older than 15 minutes)
|
||||
final cutoffTime = DateTime.now().subtract(const Duration(minutes: 15));
|
||||
|
|
@ -595,6 +592,7 @@ class MessageRetryService extends ChangeNotifier {
|
|||
if (mapping != null) {
|
||||
matchedMessageId = mapping.messageId;
|
||||
matchedAttemptIndex = mapping.attemptIndex;
|
||||
matchedPathSelection = mapping.pathSelection;
|
||||
} else {
|
||||
config?.debugLogService?.warn(
|
||||
'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex not found in direct mapping, trying fallback',
|
||||
|
|
@ -606,7 +604,7 @@ class MessageRetryService extends ChangeNotifier {
|
|||
final expectedHashes = entry.value;
|
||||
|
||||
for (final expectedHash in expectedHashes) {
|
||||
if (listEquals(expectedHash, ackHash)) {
|
||||
if (expectedHash == ackHash) {
|
||||
matchedMessageId = messageId;
|
||||
matchedAttemptIndex = expectedHashes.indexOf(expectedHash);
|
||||
break;
|
||||
|
|
@ -625,13 +623,13 @@ class MessageRetryService extends ChangeNotifier {
|
|||
}
|
||||
final contact = _pendingContacts[matchedMessageId];
|
||||
final ackedAttempt = matchedAttemptIndex ?? message.retryCount;
|
||||
final selection = _selectionFromMessage(message);
|
||||
final selection = matchedPathSelection ?? _selectionFromMessage(message);
|
||||
|
||||
final shortText = message.text.length > 20
|
||||
? '${message.text.substring(0, 20)}...'
|
||||
: message.text;
|
||||
config?.debugLogService?.info(
|
||||
'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex ✓ "$shortText" delivered to ${contact?.name ?? "unknown"} on retry ${ackedAttempt + 1} in ${tripTimeMs}ms',
|
||||
'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex ✓ "$shortText" delivered to ${contact?.name ?? "unknown"} on attempt $ackedAttempt in ${tripTimeMs}ms',
|
||||
tag: 'AckHash',
|
||||
);
|
||||
|
||||
|
|
@ -643,6 +641,8 @@ class MessageRetryService extends ChangeNotifier {
|
|||
tripTimeMs: tripTimeMs,
|
||||
);
|
||||
|
||||
final wasAlreadyResolved = _resolvedMessages.contains(matchedMessageId);
|
||||
|
||||
_cleanupMessage(matchedMessageId);
|
||||
|
||||
config?.updateMessage(deliveredMessage);
|
||||
|
|
@ -665,7 +665,9 @@ class MessageRetryService extends ChangeNotifier {
|
|||
tripTimeMs,
|
||||
);
|
||||
}
|
||||
_onMessageResolved(matchedMessageId, contact.publicKeyHex);
|
||||
if (!wasAlreadyResolved) {
|
||||
_onMessageResolved(matchedMessageId, contact.publicKeyHex);
|
||||
}
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
|
|
@ -685,11 +687,11 @@ class MessageRetryService extends ChangeNotifier {
|
|||
}
|
||||
}
|
||||
|
||||
String? getContactKeyForAckHash(Uint8List ackHash) {
|
||||
String? getContactKeyForAckHash(int ackHash) {
|
||||
for (var entry in _pendingMessages.entries) {
|
||||
final message = entry.value;
|
||||
if (message.expectedAckHash != null &&
|
||||
listEquals(message.expectedAckHash, ackHash)) {
|
||||
message.expectedAckHash == ackHash) {
|
||||
final contact = _pendingContacts[entry.key];
|
||||
return contact?.publicKeyHex;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -565,6 +565,16 @@ class PathHistoryService extends ChangeNotifier {
|
|||
_floodStats.remove(oldest);
|
||||
}
|
||||
}
|
||||
|
||||
void clearAllHistories() {
|
||||
_cache.clear();
|
||||
_cacheAccessOrder.clear();
|
||||
_autoRotationIndex.clear();
|
||||
_floodStats.clear();
|
||||
_storage.clearAllPathHistories();
|
||||
_version = 0;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class _DeferredPathRecord {
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ class UsbSerialService {
|
|||
throw StateError('USB serial port is not open');
|
||||
}
|
||||
final packet = wrapUsbSerialTxFrame(data);
|
||||
_logFrameSummary('USB TX frame', data);
|
||||
// _logFrameSummary('USB TX frame', data);
|
||||
if (_useAndroidUsbHost) {
|
||||
try {
|
||||
await _androidMethodChannel.invokeMethod<void>('write', {
|
||||
|
|
@ -447,16 +447,16 @@ class UsbSerialService {
|
|||
await _frameController.close();
|
||||
}
|
||||
|
||||
void _logFrameSummary(String prefix, Uint8List bytes) {
|
||||
if (bytes.isEmpty) {
|
||||
_debugLogService?.info('$prefix len=0', tag: 'USB Serial');
|
||||
return;
|
||||
}
|
||||
_debugLogService?.info(
|
||||
'$prefix code=${bytes[0]} len=${bytes.length}',
|
||||
tag: 'USB Serial',
|
||||
);
|
||||
}
|
||||
// void _logFrameSummary(String prefix, Uint8List bytes) {
|
||||
// if (bytes.isEmpty) {
|
||||
// _debugLogService?.info('$prefix len=0', tag: 'USB Serial');
|
||||
// return;
|
||||
// }
|
||||
// _debugLogService?.info(
|
||||
// '$prefix code=${bytes[0]} len=${bytes.length}',
|
||||
// tag: 'USB Serial',
|
||||
// );
|
||||
// }
|
||||
|
||||
/// Returns an ordered list of port paths to try for [portName].
|
||||
///
|
||||
|
|
|
|||
|
|
@ -85,9 +85,7 @@ class MessageStore {
|
|||
'messageId': msg.messageId,
|
||||
'retryCount': msg.retryCount,
|
||||
'estimatedTimeoutMs': msg.estimatedTimeoutMs,
|
||||
'expectedAckHash': msg.expectedAckHash != null
|
||||
? base64Encode(msg.expectedAckHash!)
|
||||
: null,
|
||||
'expectedAckHash': msg.expectedAckHash,
|
||||
'sentAt': msg.sentAt?.millisecondsSinceEpoch,
|
||||
'deliveredAt': msg.deliveredAt?.millisecondsSinceEpoch,
|
||||
'tripTimeMs': msg.tripTimeMs,
|
||||
|
|
@ -119,9 +117,7 @@ class MessageStore {
|
|||
messageId: json['messageId'] as String?,
|
||||
retryCount: json['retryCount'] as int? ?? 0,
|
||||
estimatedTimeoutMs: json['estimatedTimeoutMs'] as int?,
|
||||
expectedAckHash: json['expectedAckHash'] != null
|
||||
? Uint8List.fromList(base64Decode(json['expectedAckHash'] as String))
|
||||
: null,
|
||||
expectedAckHash: json['expectedAckHash'] as int? ?? 0,
|
||||
sentAt: json['sentAt'] != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(json['sentAt'] as int)
|
||||
: null,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:meshcore_open/connector/meshcore_connector.dart';
|
|||
import 'package:meshcore_open/widgets/battery_indicator.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'radio_stats_entry.dart';
|
||||
import 'snr_indicator.dart';
|
||||
|
||||
class AppBarTitle extends StatelessWidget {
|
||||
|
|
@ -10,12 +11,14 @@ class AppBarTitle extends StatelessWidget {
|
|||
final Widget? leading;
|
||||
final Widget? trailing;
|
||||
final bool indicators;
|
||||
final bool showBatteryIndicator;
|
||||
final bool subtitle;
|
||||
const AppBarTitle(
|
||||
this.title, {
|
||||
this.leading,
|
||||
this.trailing,
|
||||
this.indicators = true,
|
||||
this.showBatteryIndicator = true,
|
||||
this.subtitle = true,
|
||||
super.key,
|
||||
});
|
||||
|
|
@ -33,7 +36,7 @@ class AppBarTitle extends StatelessWidget {
|
|||
final compact = availableWidth < 170;
|
||||
final showSubtitle =
|
||||
!compact && connector.isConnected && selfName != null && subtitle;
|
||||
final showBattery = availableWidth >= 60;
|
||||
final showBattery = showBatteryIndicator && availableWidth >= 60;
|
||||
final showSnr = availableWidth >= 110;
|
||||
final showIndicators = (showBattery || showSnr) && indicators;
|
||||
|
||||
|
|
@ -60,11 +63,13 @@ class AppBarTitle extends StatelessWidget {
|
|||
if (showIndicators) const SizedBox(width: 6),
|
||||
if (showIndicators)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (showBattery) BatteryIndicator(connector: connector),
|
||||
if (showSnr) SNRIndicator(connector: connector),
|
||||
if (connector.supportsCompanionRadioStats)
|
||||
const RadioStatsIconButton(compact: true),
|
||||
],
|
||||
),
|
||||
trailing ?? const SizedBox.shrink(),
|
||||
|
|
|
|||
|
|
@ -34,11 +34,22 @@ class _PathManagementDialog extends StatefulWidget {
|
|||
class _PathManagementDialogState extends State<_PathManagementDialog> {
|
||||
bool _showAllPaths = false;
|
||||
|
||||
int _resolveContactIndex = -1;
|
||||
|
||||
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,
|
||||
orElse: () => widget.contact,
|
||||
);
|
||||
if (_resolveContactIndex == -1) {
|
||||
return widget.contact;
|
||||
}
|
||||
return connector.contacts[_resolveContactIndex];
|
||||
}
|
||||
|
||||
String _formatRelativeTime(BuildContext context, DateTime? time) {
|
||||
|
|
@ -98,6 +109,7 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
|
|||
path: Uint8List.fromList(pathBytes),
|
||||
flipPathAround: true,
|
||||
targetContact: widget.contact,
|
||||
pathHashByteWidth: connector.pathHashByteWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -124,7 +136,9 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
|
|||
connector.getContacts();
|
||||
}
|
||||
|
||||
final pathForInput = currentContact.pathIdList;
|
||||
final pathForInput = currentContact.pathFormattedIdList(
|
||||
connector.pathHashByteWidth,
|
||||
);
|
||||
final availableContacts = connector.allContacts
|
||||
.where((c) => c.publicKeyHex != currentContact.publicKeyHex)
|
||||
.toList();
|
||||
|
|
|
|||
147
lib/widgets/radio_stats_entry.dart
Normal file
147
lib/widgets/radio_stats_entry.dart
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:meshcore_open/connector/meshcore_connector.dart';
|
||||
import 'package:meshcore_open/models/companion_radio_stats.dart';
|
||||
import 'package:meshcore_open/l10n/l10n.dart';
|
||||
import 'package:meshcore_open/screens/companion_radio_stats_screen.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
void pushCompanionRadioStatsScreen(BuildContext context) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => const CompanionRadioStatsScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class RadioStatsIconButton extends StatefulWidget {
|
||||
final bool compact;
|
||||
|
||||
const RadioStatsIconButton({super.key, this.compact = false});
|
||||
|
||||
@override
|
||||
State<RadioStatsIconButton> createState() => _RadioStatsIconButtonState();
|
||||
}
|
||||
|
||||
class _RadioStatsIconButtonState extends State<RadioStatsIconButton> {
|
||||
MeshCoreConnector? _connector;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final c = context.read<MeshCoreConnector>();
|
||||
_connector = c;
|
||||
c.acquireRadioStatsPolling();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_connector?.releaseRadioStatsPolling();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<MeshCoreConnector, ({bool connected, bool supported})>(
|
||||
selector: (_, c) =>
|
||||
(connected: c.isConnected, supported: c.supportsCompanionRadioStats),
|
||||
builder: (context, state, _) {
|
||||
if (!state.connected || !state.supported) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
return ValueListenableBuilder<CompanionRadioStats?>(
|
||||
valueListenable: connector.radioStatsNotifier,
|
||||
builder: (context, _, child) {
|
||||
final dot = AirActivityDot(
|
||||
active: connector.radioStatsAirActivityPulse,
|
||||
);
|
||||
if (widget.compact) {
|
||||
return GestureDetector(
|
||||
onTap: () => pushCompanionRadioStatsScreen(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: dot,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Tooltip(
|
||||
message: context.l10n.radioStats_tooltip,
|
||||
child: InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: () => pushCompanionRadioStatsScreen(context),
|
||||
child: SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: Center(child: dot),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AirActivityDot extends StatefulWidget {
|
||||
final bool active;
|
||||
|
||||
const AirActivityDot({super.key, required this.active});
|
||||
|
||||
@override
|
||||
State<AirActivityDot> createState() => AirActivityDotState();
|
||||
}
|
||||
|
||||
class AirActivityDotState extends State<AirActivityDot> {
|
||||
Timer? _timer;
|
||||
bool _blink = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.active) _startTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant AirActivityDot oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.active && !oldWidget.active) {
|
||||
_startTimer();
|
||||
} else if (!widget.active && oldWidget.active) {
|
||||
_stopTimer();
|
||||
_blink = true;
|
||||
}
|
||||
}
|
||||
|
||||
void _startTimer() {
|
||||
_timer ??= Timer.periodic(const Duration(milliseconds: 400), (_) {
|
||||
if (!mounted) return;
|
||||
setState(() => _blink = !_blink);
|
||||
});
|
||||
}
|
||||
|
||||
void _stopTimer() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stopTimer();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final on = widget.active && _blink;
|
||||
return Icon(
|
||||
Icons.circle,
|
||||
size: 12,
|
||||
color: on ? scheme.primary : scheme.outline,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -69,11 +69,21 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
|
|||
|
||||
bool _isLoggingIn = false;
|
||||
|
||||
int _resolveRepeaterIndex = -1;
|
||||
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,
|
||||
orElse: () => widget.repeater,
|
||||
);
|
||||
if (_resolveRepeaterIndex == -1) {
|
||||
return widget.repeater;
|
||||
}
|
||||
return connector.contacts[_resolveRepeaterIndex];
|
||||
}
|
||||
|
||||
Future<void> _handleLogin() async {
|
||||
|
|
|
|||
|
|
@ -64,11 +64,22 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
|
|||
|
||||
bool _isLoggingIn = false;
|
||||
|
||||
int _resolveRepeaterIndex = -1;
|
||||
|
||||
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,
|
||||
orElse: () => widget.room,
|
||||
);
|
||||
if (_resolveRepeaterIndex == -1) {
|
||||
return widget.room;
|
||||
}
|
||||
return connector.contacts[_resolveRepeaterIndex];
|
||||
}
|
||||
|
||||
Future<void> _handleLogin() async {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import share_plus
|
|||
import shared_preferences_foundation
|
||||
import sqflite_darwin
|
||||
import url_launcher_macos
|
||||
import wakelock_plus
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin"))
|
||||
|
|
@ -24,5 +23,4 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
# 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.
|
||||
version: 7.0.0+8
|
||||
version: 7.0.0+9
|
||||
|
||||
environment:
|
||||
sdk: ^3.9.2
|
||||
|
|
@ -55,7 +55,6 @@ dependencies:
|
|||
cached_network_image: ^3.4.1
|
||||
flutter_cache_manager: ^3.4.1
|
||||
flutter_foreground_task: ^9.2.0
|
||||
wakelock_plus: ^1.4.0
|
||||
characters: ^1.4.0
|
||||
package_info_plus: ^9.0.0
|
||||
mobile_scanner: ^7.1.4 # QR/barcode scanning
|
||||
|
|
|
|||
39
test/companion_radio_stats_test.dart
Normal file
39
test/companion_radio_stats_test.dart
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
||||
import 'package:meshcore_open/models/companion_radio_stats.dart';
|
||||
|
||||
void main() {
|
||||
test('CompanionRadioStats.tryParse golden 14-byte radio frame', () {
|
||||
// noise -90 (0xA6FF LE), rssi -70 (0xBA), snr raw 8 -> 2.0 dB,
|
||||
// tx_air 1000 LE, rx_air 2000 LE
|
||||
final frame = Uint8List.fromList([
|
||||
respCodeStats,
|
||||
statsTypeRadio,
|
||||
0xA6,
|
||||
0xFF,
|
||||
0xBA,
|
||||
0x08,
|
||||
0xE8,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0xD0,
|
||||
0x07,
|
||||
0x00,
|
||||
0x00,
|
||||
]);
|
||||
final s = CompanionRadioStats.tryParse(frame);
|
||||
expect(s, isNotNull);
|
||||
expect(s!.noiseFloorDbm, -90);
|
||||
expect(s.lastRssiDbm, -70);
|
||||
expect(s.lastSnrDb, 2.0);
|
||||
expect(s.txAirSecs, 1000);
|
||||
expect(s.rxAirSecs, 2000);
|
||||
});
|
||||
|
||||
test('CompanionRadioStats.tryParse rejects short frame', () {
|
||||
expect(CompanionRadioStats.tryParse(Uint8List(10)), isNull);
|
||||
});
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ import 'package:meshcore_open/services/message_retry_service.dart';
|
|||
|
||||
/// Replicates the SHA-256 computation from [MessageRetryService.computeExpectedAckHash]
|
||||
/// so tests can cross-check without calling the real implementation twice.
|
||||
Uint8List _manualAckHash(
|
||||
int _manualAckHash(
|
||||
int timestampSeconds,
|
||||
int attemptMasked, // already masked to 0x03
|
||||
String text,
|
||||
|
|
@ -35,7 +35,8 @@ Uint8List _manualAckHash(
|
|||
buffer.setRange(offset, offset + senderPubKey.length, senderPubKey);
|
||||
|
||||
final hash = sha256.convert(buffer);
|
||||
return Uint8List.fromList(hash.bytes.sublist(0, 4));
|
||||
final bytes = Uint8List.fromList(hash.bytes.sublist(0, 4));
|
||||
return (bytes[3] << 24) | (bytes[2] << 16) | (bytes[1] << 8) | bytes[0];
|
||||
}
|
||||
|
||||
Uint8List _makeKey(int seed) {
|
||||
|
|
@ -169,16 +170,6 @@ void main() {
|
|||
expect(first, equals(second));
|
||||
});
|
||||
|
||||
test('hash is exactly 4 bytes long', () {
|
||||
final hash = MessageRetryService.computeExpectedAckHash(
|
||||
fixedTs,
|
||||
0,
|
||||
fixedText,
|
||||
fixedKey,
|
||||
);
|
||||
expect(hash.length, equals(4));
|
||||
});
|
||||
|
||||
test('hash matches manual SHA-256 computation', () {
|
||||
for (int attempt = 0; attempt < 4; attempt++) {
|
||||
final actual = MessageRetryService.computeExpectedAckHash(
|
||||
|
|
@ -509,7 +500,7 @@ void main() {
|
|||
fixedText,
|
||||
fixedKey,
|
||||
);
|
||||
final hex = hash.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
||||
final hex = hash.toRadixString(16).padLeft(8, '0');
|
||||
expect(
|
||||
hashes.containsKey(hex),
|
||||
isFalse,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ Usage:
|
|||
|
||||
# Translate all locales (missing strings only):
|
||||
python translate.py --in lib/l10n/app_en.arb --l10n-dir lib/l10n --missing-only
|
||||
|
||||
# New locales copied from app_en.arb still match English → --missing-only skips them.
|
||||
# Translate every key that still equals the template (e.g. hu, ja, ko):
|
||||
python translate.py --in lib/l10n/app_en.arb --l10n-dir lib/l10n --copy-of-template --only-locales hu,ja,ko
|
||||
"""
|
||||
|
||||
import argparse
|
||||
|
|
@ -68,6 +72,7 @@ LOCALE_MAP = {
|
|||
"sk": ("Slovak", "sk"),
|
||||
"sl": ("Slovenian", "sl"),
|
||||
"bg": ("Bulgarian", "bg"),
|
||||
"hu": ("Hungarian", "hu"),
|
||||
"el": ("Greek", "el"),
|
||||
"he": ("Hebrew", "he"),
|
||||
"th": ("Thai", "th"),
|
||||
|
|
@ -261,6 +266,25 @@ def find_missing_keys(source_data: Dict[str, Any], target_data: Dict[str, Any])
|
|||
return missing
|
||||
|
||||
|
||||
def find_keys_still_template_copy(source_data: Dict[str, Any], target_data: Dict[str, Any]) -> List[str]:
|
||||
"""Keys whose value is still exactly the same as the template (typical after cp app_en.arb → app_xx.arb)."""
|
||||
out: List[str] = []
|
||||
for key in source_data:
|
||||
if key == "@@locale" or key.startswith("@"):
|
||||
continue
|
||||
src = source_data.get(key)
|
||||
if not is_translatable_entry(key, src):
|
||||
continue
|
||||
if not isinstance(src, str):
|
||||
continue
|
||||
tgt = target_data.get(key)
|
||||
if not isinstance(tgt, str) or tgt.strip() == "":
|
||||
out.append(key)
|
||||
elif tgt == src:
|
||||
out.append(key)
|
||||
return out
|
||||
|
||||
|
||||
def get_all_locale_files(l10n_dir: str, template_file: str) -> List[Tuple[str, str]]:
|
||||
"""Find all locale .arb files excluding template. Returns [(locale_code, file_path)]."""
|
||||
locales = []
|
||||
|
|
@ -434,6 +458,15 @@ def main() -> int:
|
|||
ap.add_argument("--to-locale", help="Target locale code (es, fr, de, etc.)")
|
||||
ap.add_argument("--l10n-dir", help="Directory with locale files (translates all locales)")
|
||||
ap.add_argument("--missing-only", action="store_true", help="Only translate missing keys")
|
||||
ap.add_argument(
|
||||
"--copy-of-template",
|
||||
action="store_true",
|
||||
help="Only translate keys whose target text still equals app_en (use for new locales copied from English)",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--only-locales",
|
||||
help="Comma-separated locale codes to process with --l10n-dir (e.g. hu,ja,ko)",
|
||||
)
|
||||
ap.add_argument("--model", default="translategemma:latest", help="Ollama model (translategemma:latest or specific versions)")
|
||||
ap.add_argument("--fallback-model", help="Fallback model for failed translations (e.g., translategemma:27b)")
|
||||
ap.add_argument("--host", default="http://localhost:11434", help="Ollama host")
|
||||
|
|
@ -458,6 +491,14 @@ def main() -> int:
|
|||
print("Input JSON must be an object at top-level.", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
if args.missing_only and args.copy_of_template:
|
||||
print("Use only one of --missing-only or --copy-of-template", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
only_locales: Optional[set] = None
|
||||
if args.only_locales:
|
||||
only_locales = {x.strip() for x in args.only_locales.split(",") if x.strip()}
|
||||
|
||||
# Process all locales if --l10n-dir is provided
|
||||
if args.l10n_dir:
|
||||
locales = get_all_locale_files(args.l10n_dir, args.in_path)
|
||||
|
|
@ -465,6 +506,12 @@ def main() -> int:
|
|||
print(f"No locale files found in {args.l10n_dir}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if only_locales is not None:
|
||||
locales = [(c, p) for c, p in locales if c in only_locales]
|
||||
missing = only_locales - {c for c, _ in locales}
|
||||
if missing:
|
||||
print(f"Warning: no app_*.arb for locale code(s): {', '.join(sorted(missing))}", file=sys.stderr)
|
||||
|
||||
print(f"Found {len(locales)} locale file(s) to process")
|
||||
|
||||
total_translated = 0
|
||||
|
|
@ -478,7 +525,14 @@ def main() -> int:
|
|||
print(f" [{locale_code}] Failed to read {locale_path}: {e}")
|
||||
continue
|
||||
|
||||
if args.missing_only:
|
||||
missing_keys: Optional[List[str]]
|
||||
if args.copy_of_template:
|
||||
missing_keys = find_keys_still_template_copy(source_data, target_data)
|
||||
if not missing_keys:
|
||||
print(f" [{locale_code}] No keys still matching template")
|
||||
continue
|
||||
print(f" [{locale_code}] {len(missing_keys)} key(s) still same as template")
|
||||
elif args.missing_only:
|
||||
missing_keys = find_missing_keys(source_data, target_data)
|
||||
if not missing_keys:
|
||||
print(f" [{locale_code}] No missing keys")
|
||||
|
|
@ -509,18 +563,23 @@ def main() -> int:
|
|||
|
||||
lang_name, lang_code = LOCALE_MAP.get(args.to_locale, (args.to_locale, args.to_locale))
|
||||
|
||||
# Read existing target file if --missing-only
|
||||
# Read existing target file if --missing-only or --copy-of-template
|
||||
target_data: Dict[str, Any] = {}
|
||||
missing_keys: Optional[List[str]] = None
|
||||
if args.missing_only and os.path.exists(args.out_path):
|
||||
if (args.missing_only or args.copy_of_template) and os.path.exists(args.out_path):
|
||||
try:
|
||||
with open(args.out_path, "r", encoding="utf-8") as f:
|
||||
target_data = json.load(f)
|
||||
missing_keys = find_missing_keys(source_data, target_data)
|
||||
if args.copy_of_template:
|
||||
missing_keys = find_keys_still_template_copy(source_data, target_data)
|
||||
label = "still matching template"
|
||||
else:
|
||||
missing_keys = find_missing_keys(source_data, target_data)
|
||||
label = "missing"
|
||||
if not missing_keys:
|
||||
print(f"No missing keys in {args.out_path}")
|
||||
print(f"No {label} keys in {args.out_path}")
|
||||
return 0
|
||||
print(f"Found {len(missing_keys)} missing key(s) to translate")
|
||||
print(f"Found {len(missing_keys)} {label} key(s) to translate")
|
||||
except Exception as e:
|
||||
print(f"Failed to read target file: {e}", file=sys.stderr)
|
||||
return 2
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue