mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-08-13 03:06:26 +10:00
Merge main into unread-peoplefirst
Resolved conflicts by accepting refactored state management from main: - list_filter_widget.dart: Adopt sealed class pattern for filter actions - contacts_screen.dart: Move state to UiViewStateService instead of local setState - device_screen.dart: Accept deletion (consolidated into other screens in main) Main branch includes significant improvements: - TCP and USB transport support - Service-based state management with UiViewStateService - Translation support with message translation buttons - Signal UI consistency improvements - Additional language support (hu, ja, ko) - Comprehensive test coverage - Discovery screen refactoring
This commit is contained in:
+2798
-577
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,70 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../services/app_debug_log_service.dart';
|
||||
import '../services/tcp_transport_service.dart';
|
||||
|
||||
/// Manages TCP transport for MeshCore devices.
|
||||
///
|
||||
/// Owns the [TcpTransportService] and TCP-specific connection state.
|
||||
/// The main [MeshCoreConnector] delegates all TCP operations here.
|
||||
class MeshCoreTcpConnector {
|
||||
final TcpTransportService _service = TcpTransportService();
|
||||
AppDebugLogService? _debugLog;
|
||||
StreamSubscription<Uint8List>? _frameSubscription;
|
||||
|
||||
// --- Getters ---
|
||||
String? get activeEndpoint => _service.activeEndpoint;
|
||||
bool get isConnected => _service.isConnected;
|
||||
|
||||
// --- Configuration ---
|
||||
void setDebugLogService(AppDebugLogService? service) {
|
||||
_debugLog = service;
|
||||
_service.setDebugLogService(service);
|
||||
}
|
||||
|
||||
// --- Connection lifecycle ---
|
||||
Future<void> connect({required String host, required int port}) async {
|
||||
_debugLog?.info('TcpConnector.connect endpoint=$host:$port', tag: 'TCP');
|
||||
await _frameSubscription?.cancel();
|
||||
_frameSubscription = null;
|
||||
await _service.connect(host: host, port: port);
|
||||
_debugLog?.info(
|
||||
'TcpConnector.connect done, endpoint=${_service.activeEndpoint}',
|
||||
tag: 'TCP',
|
||||
);
|
||||
}
|
||||
|
||||
StreamSubscription<Uint8List> listenFrames({
|
||||
required void Function(Uint8List) onFrame,
|
||||
required void Function(Object, StackTrace?) onError,
|
||||
required void Function() onDone,
|
||||
}) {
|
||||
_frameSubscription = _service.frameStream.listen(
|
||||
onFrame,
|
||||
onError: onError,
|
||||
onDone: onDone,
|
||||
);
|
||||
return _frameSubscription!;
|
||||
}
|
||||
|
||||
Future<void> cancelFrameSubscription() async {
|
||||
await _frameSubscription?.cancel();
|
||||
_frameSubscription = null;
|
||||
}
|
||||
|
||||
Future<void> disconnect() async {
|
||||
if (!_service.isConnected && _frameSubscription == null) return;
|
||||
_debugLog?.info('TcpConnector.disconnect', tag: 'TCP');
|
||||
await _frameSubscription?.cancel();
|
||||
_frameSubscription = null;
|
||||
await _service.disconnect();
|
||||
}
|
||||
|
||||
Future<void> write(Uint8List data) => _service.write(data);
|
||||
|
||||
void dispose() {
|
||||
_frameSubscription?.cancel();
|
||||
_service.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../services/app_debug_log_service.dart';
|
||||
import '../services/usb_serial_service.dart';
|
||||
|
||||
/// Manages USB serial transport for MeshCore devices.
|
||||
///
|
||||
/// Owns the [UsbSerialService] and USB-specific connection state.
|
||||
/// The main [MeshCoreConnector] delegates all USB operations here.
|
||||
class MeshCoreUsbManager {
|
||||
MeshCoreUsbManager();
|
||||
|
||||
final UsbSerialService _service = UsbSerialService();
|
||||
AppDebugLogService? _debugLog;
|
||||
String? _activePortKey;
|
||||
String? _activePortLabel;
|
||||
|
||||
// --- Getters ---
|
||||
String? get activePortKey => _activePortKey;
|
||||
String? get activePortDisplayLabel => _activePortLabel ?? _activePortKey;
|
||||
bool get isConnected => _service.isConnected;
|
||||
Stream<Uint8List> get frameStream => _service.frameStream;
|
||||
|
||||
// --- Configuration ---
|
||||
Future<List<String>> listPorts() => _service.listPorts();
|
||||
|
||||
void setRequestPortLabel(String label) => _service.setRequestPortLabel(label);
|
||||
|
||||
void setFallbackDeviceName(String label) =>
|
||||
_service.setFallbackDeviceName(label);
|
||||
|
||||
void setDebugLogService(AppDebugLogService? service) {
|
||||
_debugLog = service;
|
||||
_service.setDebugLogService(service);
|
||||
}
|
||||
|
||||
// --- Connection lifecycle ---
|
||||
Future<void> connect({
|
||||
required String portName,
|
||||
int baudRate = 115200,
|
||||
}) async {
|
||||
_debugLog?.info(
|
||||
'UsbManager.connect: portName=$portName baud=$baudRate',
|
||||
tag: 'USB',
|
||||
);
|
||||
await _service.connect(portName: portName, baudRate: baudRate);
|
||||
_activePortKey = _service.activePortKey ?? portName;
|
||||
_activePortLabel = _service.activePortDisplayLabel ?? portName;
|
||||
_debugLog?.info(
|
||||
'UsbManager.connect: done, key=$_activePortKey label=$_activePortLabel',
|
||||
tag: 'USB',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> disconnect() async {
|
||||
if (!_service.isConnected && _activePortKey == null) {
|
||||
return;
|
||||
}
|
||||
_debugLog?.info('UsbManager.disconnect', tag: 'USB');
|
||||
await _service.disconnect();
|
||||
_activePortKey = null;
|
||||
_activePortLabel = null;
|
||||
}
|
||||
|
||||
Future<void> write(Uint8List data) => _service.write(data);
|
||||
|
||||
Future<void> writeRaw(Uint8List data) => _service.writeRaw(data);
|
||||
|
||||
// --- Label management ---
|
||||
void updateConnectedLabel(String selfName) {
|
||||
_service.updateConnectedLabel(selfName);
|
||||
_activePortLabel = _service.activePortDisplayLabel ?? _activePortLabel;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_service.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
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;
|
||||
int _lastPointer = 0;
|
||||
final Uint8List _buffer;
|
||||
|
||||
BufferReader(Uint8List data) : _buffer = Uint8List.fromList(data);
|
||||
@@ -13,6 +16,7 @@ class BufferReader {
|
||||
int readByte() => readBytes(1)[0];
|
||||
|
||||
Uint8List readBytes(int count) {
|
||||
_lastPointer = _pointer;
|
||||
if (_pointer + count > _buffer.length) {
|
||||
throw RangeError(
|
||||
'Attempted to read $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
|
||||
@@ -24,6 +28,7 @@ class BufferReader {
|
||||
}
|
||||
|
||||
void skipBytes(int count) {
|
||||
_lastPointer = _pointer;
|
||||
if (_pointer + count > _buffer.length) {
|
||||
throw RangeError(
|
||||
'Attempted to skip $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
|
||||
@@ -34,10 +39,8 @@ class BufferReader {
|
||||
|
||||
Uint8List readRemainingBytes() => readBytes(remaining);
|
||||
|
||||
String readString() =>
|
||||
utf8.decode(readRemainingBytes(), allowMalformed: true);
|
||||
|
||||
String readCString(int maxLength) {
|
||||
String readCStringGreedy(int maxLength) {
|
||||
_lastPointer = _pointer;
|
||||
final value = <int>[];
|
||||
final bytes = readBytes(maxLength);
|
||||
for (final byte in bytes) {
|
||||
@@ -51,6 +54,25 @@ class BufferReader {
|
||||
}
|
||||
}
|
||||
|
||||
String readCString({int maxLength = -1}) {
|
||||
final backupPointer = _pointer;
|
||||
final value = <int>[];
|
||||
int counter = 0;
|
||||
final maxLen = maxLength >= 0 ? maxLength : remaining;
|
||||
while (counter < maxLen) {
|
||||
final byte = readByte();
|
||||
if (byte == 0) break;
|
||||
value.add(byte);
|
||||
counter++;
|
||||
}
|
||||
_lastPointer = backupPointer;
|
||||
try {
|
||||
return utf8.decode(Uint8List.fromList(value), allowMalformed: true);
|
||||
} catch (e) {
|
||||
return String.fromCharCodes(value); // Latin-1 fallback
|
||||
}
|
||||
}
|
||||
|
||||
int readUInt8() => readBytes(1).buffer.asByteData().getUint8(0);
|
||||
int readInt8() => readBytes(1).buffer.asByteData().getInt8(0);
|
||||
int readUInt16LE() =>
|
||||
@@ -72,6 +94,9 @@ class BufferReader {
|
||||
if ((value & 0x800000) != 0) value -= 0x1000000;
|
||||
return value;
|
||||
}
|
||||
|
||||
void resetPointer() => _pointer = 0;
|
||||
void rewind() => _pointer = _lastPointer;
|
||||
}
|
||||
|
||||
// Buffer Writer - accumulating binary data builder
|
||||
@@ -114,23 +139,38 @@ class BufferWriter {
|
||||
}
|
||||
|
||||
void writeHex(String hex) {
|
||||
// Validate hex string length is even and not empty
|
||||
if (hex.isEmpty || hex.length % 2 != 0) {
|
||||
throw FormatException('Invalid hex string length: ${hex.length}');
|
||||
}
|
||||
List<int> result = [];
|
||||
for (int i = 0; i < hex.length ~/ 2; i++) {
|
||||
final hexByte = hex.substring(i * 2, i * 2 + 2);
|
||||
final byte = int.tryParse(hexByte, radix: 16);
|
||||
if (byte == null) {
|
||||
throw FormatException(
|
||||
'Invalid hex characters at position $i: $hexByte',
|
||||
);
|
||||
}
|
||||
result.add(byte);
|
||||
}
|
||||
writeBytes(Uint8List.fromList(result));
|
||||
writeBytes(hex2Uint8List(hex));
|
||||
}
|
||||
|
||||
void writeBytesPadded(Uint8List bytes, int totalLength) {
|
||||
// Path data (64 bytes, zero-padded)
|
||||
final bytesPadded = Uint8List(totalLength);
|
||||
final len = bytes.length < totalLength ? bytes.length : totalLength;
|
||||
if (bytes.isNotEmpty && len > 0) {
|
||||
final copyLen = bytes.length < totalLength ? bytes.length : totalLength;
|
||||
for (int i = 0; i < copyLen; i++) {
|
||||
bytesPadded[i] = bytes[i];
|
||||
}
|
||||
}
|
||||
writeBytes(bytesPadded);
|
||||
}
|
||||
}
|
||||
|
||||
Uint8List hex2Uint8List(String hex) {
|
||||
// Validate hex string length is even and not empty
|
||||
if (hex.isEmpty || hex.length % 2 != 0) {
|
||||
throw FormatException('Invalid hex string length: ${hex.length}');
|
||||
}
|
||||
List<int> result = [];
|
||||
for (int i = 0; i < hex.length ~/ 2; i++) {
|
||||
final hexByte = hex.substring(i * 2, i * 2 + 2);
|
||||
final byte = int.tryParse(hexByte, radix: 16);
|
||||
if (byte == null) {
|
||||
throw FormatException('Invalid hex characters at position $i: $hexByte');
|
||||
}
|
||||
result.add(byte);
|
||||
}
|
||||
return Uint8List.fromList(result);
|
||||
}
|
||||
|
||||
// Command codes (to device)
|
||||
@@ -162,15 +202,20 @@ const int cmdGetChannel = 31;
|
||||
const int cmdSetChannel = 32;
|
||||
const int cmdSendTracePath = 36;
|
||||
const int cmdSetOtherParams = 38;
|
||||
const int cmdGetRadioSettings = 57;
|
||||
const int cmdGetTelemetryReq = 39;
|
||||
const int cmdSendTelemetryReq = 39;
|
||||
const int cmdGetCustomVar = 40;
|
||||
const int cmdSetCustomVar = 41;
|
||||
const int cmdSendBinaryReq = 50;
|
||||
const int cmdGetStats = 56;
|
||||
const int cmdSendAnonReq = 57;
|
||||
const int cmdSetAutoAddConfig = 58;
|
||||
const int cmdGetAutoAddConfig = 59;
|
||||
const int cmdSetPathHashMode = 61;
|
||||
|
||||
// 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;
|
||||
@@ -200,8 +245,13 @@ const int respCodeDeviceInfo = 13;
|
||||
const int respCodeContactMsgRecvV3 = 16;
|
||||
const int respCodeChannelMsgRecvV3 = 17;
|
||||
const int respCodeChannelInfo = 18;
|
||||
const int respCodeRadioSettings = 25;
|
||||
const int respCodeCustomVars = 21;
|
||||
const int respCodeAutoAddConfig = 25;
|
||||
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;
|
||||
@@ -223,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)
|
||||
@@ -247,8 +301,21 @@ const int payloadTypeCONTROL = 0x0B; // a control/discovery packet
|
||||
const int payloadTypeRawCustom =
|
||||
0x0F; // custom packet as raw bytes, for applications with custom encryption, payloads, etc
|
||||
|
||||
//auto-add flags
|
||||
const int autoAddOverwriteOldestFlag =
|
||||
1 << 0; // 0x01 - overwrite oldest non-favourite when full
|
||||
const int autoAddChatFlag =
|
||||
1 << 1; // 0x02 - auto-add Chat (Companion) (ADV_TYPE_CHAT)
|
||||
const int autoAddRepeaterFlag =
|
||||
1 << 2; // 0x04 - auto-add Repeater (ADV_TYPE_REPEATER)
|
||||
const int autoAddRoomServerFlag =
|
||||
1 << 3; // 0x08 - auto-add Room Server (ADV_TYPE_ROOM)
|
||||
const int autoAddSensorFlag =
|
||||
1 << 4; // 0x10 - auto-add Sensor (ADV_TYPE_SENSOR)
|
||||
|
||||
// Sizes
|
||||
const int pubKeySize = 32;
|
||||
const int signatureSize = 64;
|
||||
const int maxPathSize = 64;
|
||||
const int pathHashSize = 1;
|
||||
const int maxNameSize = 32;
|
||||
@@ -291,13 +358,16 @@ 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;
|
||||
const int contactTimestampOffset = 132;
|
||||
const int contactLatOffset = 136;
|
||||
const int contactLonOffset = 140;
|
||||
const int contactLastmodOffset = 144;
|
||||
const int contactLastModOffset = 144;
|
||||
const int contactFrameSize = 148;
|
||||
|
||||
// Message frame offsets
|
||||
@@ -309,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
|
||||
@@ -377,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
|
||||
@@ -448,7 +501,7 @@ Uint8List buildSendTextMsgFrame(
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdSendTxtMsg);
|
||||
writer.writeByte(txtTypePlain);
|
||||
writer.writeByte(attempt.clamp(0, 3));
|
||||
writer.writeByte(attempt.clamp(0, 255));
|
||||
writer.writeUInt32LE(timestamp);
|
||||
writer.writeBytes(recipientPubKey.sublist(0, 6));
|
||||
writer.writeString(text);
|
||||
@@ -508,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();
|
||||
@@ -628,14 +692,17 @@ Uint8List buildResetPathFrame(Uint8List pubKey) {
|
||||
}
|
||||
|
||||
// Build CMD_ADD_UPDATE_CONTACT frame to set custom path
|
||||
// Format: [cmd][pub_key x32][type][flags][path_len][path x64][name x32][timestamp x4]
|
||||
// Format: [cmd][pub_key x32][type][flags][path_len][path x64][name x32][Lat? x4, Lon? x4][timestamp? x4]
|
||||
Uint8List buildUpdateContactPathFrame(
|
||||
Uint8List pubKey,
|
||||
Uint8List customPath,
|
||||
Uint8List path,
|
||||
int pathLen, {
|
||||
int type = 1, // ADV_TYPE_CHAT
|
||||
int flags = 0,
|
||||
String name = '',
|
||||
double? lat,
|
||||
double? lon,
|
||||
DateTime? lastModified,
|
||||
}) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdAddUpdateContact);
|
||||
@@ -644,17 +711,7 @@ Uint8List buildUpdateContactPathFrame(
|
||||
writer.writeByte(flags);
|
||||
writer.writeByte(pathLen);
|
||||
|
||||
// Path data (64 bytes, zero-padded)
|
||||
final pathPadded = Uint8List(maxPathSize);
|
||||
if (customPath.isNotEmpty && pathLen > 0) {
|
||||
final copyLen = customPath.length < maxPathSize
|
||||
? customPath.length
|
||||
: maxPathSize;
|
||||
for (int i = 0; i < copyLen; i++) {
|
||||
pathPadded[i] = customPath[i];
|
||||
}
|
||||
}
|
||||
writer.writeBytes(pathPadded);
|
||||
writer.writeBytesPadded(path, maxPathSize);
|
||||
|
||||
// Name (32 bytes, null-padded)
|
||||
writer.writeCString(name, maxNameSize);
|
||||
@@ -663,6 +720,27 @@ Uint8List buildUpdateContactPathFrame(
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
writer.writeUInt32LE(timestamp);
|
||||
|
||||
if ((lat == null || lon == null) && lastModified != null) {
|
||||
// If lat/lon not provided, write zeros
|
||||
writer.writeInt32LE(0);
|
||||
writer.writeInt32LE(0);
|
||||
} else {
|
||||
// Latitude and Longitude are expected in degrees, convert to int by multiplying by 1e6
|
||||
// Latitude
|
||||
final latitude = lat ?? 0.0;
|
||||
writer.writeInt32LE((latitude * 1e6).round());
|
||||
|
||||
// Longitude
|
||||
final longitude = lon ?? 0.0;
|
||||
writer.writeInt32LE((longitude * 1e6).round());
|
||||
}
|
||||
|
||||
if (lastModified != null) {
|
||||
// Last modified
|
||||
final lastModifiedTimestamp = lastModified.millisecondsSinceEpoch ~/ 1000;
|
||||
writer.writeUInt32LE(lastModifiedTimestamp);
|
||||
}
|
||||
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
@@ -675,16 +753,15 @@ Uint8List buildGetContactByKeyFrame(Uint8List pubKey) {
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
// Build CMD_GET_RADIO_SETTINGS frame
|
||||
Uint8List buildGetRadioSettingsFrame() {
|
||||
return Uint8List.fromList([cmdGetRadioSettings]);
|
||||
}
|
||||
|
||||
//Build CMD_GET_CUSTOM_VARS frame
|
||||
Uint8List buildGetCustomVarsFrame() {
|
||||
return Uint8List.fromList([cmdGetCustomVar]);
|
||||
}
|
||||
|
||||
Uint8List buildGetAutoAddFlagsFrame() {
|
||||
return Uint8List.fromList([cmdGetAutoAddConfig]);
|
||||
}
|
||||
|
||||
// Calculate LoRa airtime for a packet
|
||||
// Based on Semtech SX127x datasheet formula
|
||||
// Returns airtime in milliseconds
|
||||
@@ -764,7 +841,7 @@ Uint8List buildSendCliCommandFrame(
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdSendTxtMsg);
|
||||
writer.writeByte(txtTypeCliData);
|
||||
writer.writeByte(attempt.clamp(0, 3));
|
||||
writer.writeByte(attempt.clamp(0, 255));
|
||||
writer.writeUInt32LE(timestamp);
|
||||
writer.writeBytes(repeaterPubKey.sublist(0, 6));
|
||||
writer.writeString(command);
|
||||
@@ -809,10 +886,10 @@ Uint8List buildExportContactFrame(Uint8List pubKey) {
|
||||
|
||||
// Build a import contact frame
|
||||
// [cmd][contact_frame x98+]
|
||||
Uint8List buildImportContactFrame(String contactFrame) {
|
||||
Uint8List buildImportContactFrame(Uint8List contactFrame) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdImportContact);
|
||||
writer.writeHex(contactFrame);
|
||||
writer.writeBytes(contactFrame);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
@@ -826,20 +903,55 @@ Uint8List buildZeroHopContact(Uint8List pubKey) {
|
||||
}
|
||||
|
||||
// Build CMD_SET_OTHER_PARAMS frame
|
||||
// Format: [cmd][allowAutoAddContacts][allowTelemetryFlags][advertLocationPolicy][multiAcks]
|
||||
// Format: [cmd][allowTelemetryFlags][advertLocationPolicy][multiAcks]
|
||||
Uint8List buildSetOtherParamsFrame(
|
||||
bool allowAutoAddContacts,
|
||||
int allowTelemetryFlags,
|
||||
int advertLocationPolicy,
|
||||
int multiAcks,
|
||||
) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdSetOtherParams);
|
||||
writer.writeByte(
|
||||
allowAutoAddContacts ? 0x00 : 0x01,
|
||||
); // Allow Auto Add Contacts
|
||||
//Going forward the app will just set Auto Add Contacts to disabled, and use the filter flags
|
||||
//Allow Auto Add Contacts use inverted logic (0x01 = disabled, 0x00 = enabled).
|
||||
writer.writeByte(0x01);
|
||||
writer.writeByte(allowTelemetryFlags); // Allow Telemetry Flags
|
||||
writer.writeByte(advertLocationPolicy); // Advertisement Location Policy
|
||||
writer.writeByte(multiAcks); // Multi Acknowledgements
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
// Build CMD_SET_AUTO_ADD_CONFIG frame
|
||||
// Format: [cmd][flags]
|
||||
Uint8List buildSetAutoAddConfigFrame({
|
||||
required bool autoAddChat,
|
||||
required bool autoAddRepeater,
|
||||
required bool autoAddRoomServer,
|
||||
required bool autoAddSensor,
|
||||
required bool overwriteOldest,
|
||||
}) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdSetAutoAddConfig);
|
||||
int flags = 0;
|
||||
if (autoAddChat) flags |= autoAddChatFlag;
|
||||
if (autoAddRepeater) flags |= autoAddRepeaterFlag;
|
||||
if (autoAddRoomServer) flags |= autoAddRoomServerFlag;
|
||||
if (autoAddSensor) flags |= autoAddSensorFlag;
|
||||
if (overwriteOldest) flags |= autoAddOverwriteOldestFlag;
|
||||
writer.writeByte(flags);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
//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();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
class MeshCoreUuids {
|
||||
static const String service = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
|
||||
static const String rxCharacteristic = "6e400002-b5a3-f393-e0a9-e50e24dcca9e";
|
||||
static const String txCharacteristic = "6e400003-b5a3-f393-e0a9-e50e24dcca9e";
|
||||
|
||||
static const List<String> deviceNamePrefixes = [
|
||||
"MeshCore-",
|
||||
"Whisper-",
|
||||
"WisCore-",
|
||||
"Seeed",
|
||||
"Lilygo",
|
||||
"HT-",
|
||||
"LowMesh_MC_",
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
class GifHelper {
|
||||
/// Parse a known GIF format, which can be any of:
|
||||
/// g:GIFID
|
||||
/// https://media.giphy.com/media/GIFID/giphy.gif
|
||||
/// https://giphy.com/gifs/Optional-title-with-dashes-GIFID
|
||||
///
|
||||
/// GIFID is a Giphy GIF ID. The https:// is optional (and
|
||||
/// can also be http://). The giphy.com/gifs form can also
|
||||
/// include a trailing slash.
|
||||
///
|
||||
/// Returns null if text is not a valid GIF format
|
||||
static String? parseGif(String text) {
|
||||
final trimmed = text.trim();
|
||||
final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed);
|
||||
if (match != null) {
|
||||
return match.group(1);
|
||||
}
|
||||
final directUrlMatch = RegExp(
|
||||
r'^(?:https?:\/\/)?media\.giphy\.com\/media\/([A-Za-z0-9_-]+)\/giphy\.gif$',
|
||||
).firstMatch(trimmed);
|
||||
if (directUrlMatch != null) {
|
||||
return directUrlMatch.group(1);
|
||||
}
|
||||
// Giphy understands page URLs with just the ID, or any string and a
|
||||
// dash before the ID, and redirects to a page with a dash-separated
|
||||
// title, a dash, and the ID. IDs in this form *probably* can't
|
||||
// contain dashes.
|
||||
final pageMatch = RegExp(
|
||||
r'^(?:https?:\/\/)?giphy\.com\/gifs\/(?:[^/?]*-)?([A-Za-z0-9_]+)\/?$',
|
||||
).firstMatch(trimmed);
|
||||
return pageMatch?.group(1);
|
||||
}
|
||||
|
||||
/// Encode a GIF in a format that parseGif() can parse.
|
||||
static String encodeGif(String gifId) {
|
||||
return 'g:$gifId';
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../utils/platform_info.dart';
|
||||
import '../helpers/snack_bar_builder.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,
|
||||
required String text,
|
||||
required TextStyle style,
|
||||
TextStyle? linkStyle,
|
||||
}) {
|
||||
final effectiveLinkStyle = linkStyle ?? defaultLinkStyle(context, style);
|
||||
const options = LinkifyOptions(humanize: false, defaultToHttps: false);
|
||||
const linkifiers = [UrlLinkifier(), EmailLinkifier()];
|
||||
void onOpen(LinkableElement link) => handleLinkTap(context, link.url);
|
||||
|
||||
if (PlatformInfo.isDesktop) {
|
||||
return SelectableLinkify(
|
||||
text: text,
|
||||
style: style,
|
||||
linkStyle: effectiveLinkStyle,
|
||||
options: options,
|
||||
linkifiers: linkifiers,
|
||||
onOpen: onOpen,
|
||||
);
|
||||
}
|
||||
return Linkify(
|
||||
text: text,
|
||||
style: style,
|
||||
linkStyle: effectiveLinkStyle,
|
||||
options: options,
|
||||
linkifiers: linkifiers,
|
||||
onOpen: onOpen,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> handleLinkTap(BuildContext context, String url) async {
|
||||
// Show confirmation dialog
|
||||
final shouldOpen = await showDialog<bool>(
|
||||
@@ -51,21 +94,19 @@ class LinkHandler {
|
||||
final uri = Uri.parse(url);
|
||||
if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.chat_couldNotOpenLink(url)),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.chat_couldNotOpenLink(url)),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.chat_invalidLink),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.chat_invalidLink),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import '../models/contact.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
|
||||
class PathHelper {
|
||||
static String formatPathHex(List<int> pathBytes) {
|
||||
return pathBytes
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||
.join(',');
|
||||
}
|
||||
|
||||
static String resolvePathNames(
|
||||
List<int> pathBytes,
|
||||
List<Contact> allContacts,
|
||||
) {
|
||||
return pathBytes
|
||||
.map((b) {
|
||||
final hex = b.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||
final matches = allContacts
|
||||
.where(
|
||||
(c) =>
|
||||
c.publicKey.first == b &&
|
||||
(c.type == advTypeRepeater || c.type == advTypeRoom),
|
||||
)
|
||||
.toList();
|
||||
if (matches.isEmpty) return hex;
|
||||
if (matches.length == 1) return matches.first.name;
|
||||
return matches.map((c) => c.name).join(' | ');
|
||||
})
|
||||
.join(' \u2192 ');
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,50 @@ class ReactionInfo {
|
||||
}
|
||||
|
||||
class ReactionHelper {
|
||||
/// Apply a reaction to a list of messages by matching the reaction hash.
|
||||
///
|
||||
/// [messages] - the message list to search
|
||||
/// [reactionInfo] - the parsed reaction
|
||||
/// [getTimestampSecs] - extract timestamp seconds from a message
|
||||
/// [getSenderName] - extract sender name for hash (null for 1:1 implicit)
|
||||
/// [getMessageText] - extract message text
|
||||
/// [getReactions] - extract current reactions map
|
||||
/// [shouldSkip] - filter function to skip messages (e.g., skip outgoing for incoming reactions)
|
||||
/// [updateMessage] - callback to update the message at index with new reactions
|
||||
///
|
||||
/// Returns whether a match was found.
|
||||
static bool applyReaction<T>({
|
||||
required List<T> messages,
|
||||
required ReactionInfo reactionInfo,
|
||||
required int Function(T) getTimestampSecs,
|
||||
required String? Function(T) getSenderName,
|
||||
required String Function(T) getMessageText,
|
||||
required Map<String, int> Function(T) getReactions,
|
||||
required bool Function(T) shouldSkip,
|
||||
required void Function(int index, Map<String, int> newReactions)
|
||||
updateMessage,
|
||||
}) {
|
||||
final targetHash = reactionInfo.targetHash;
|
||||
for (int i = messages.length - 1; i >= 0; i--) {
|
||||
final msg = messages[i];
|
||||
if (shouldSkip(msg)) continue;
|
||||
|
||||
final msgHash = computeReactionHash(
|
||||
getTimestampSecs(msg),
|
||||
getSenderName(msg),
|
||||
getMessageText(msg),
|
||||
);
|
||||
if (msgHash == targetHash) {
|
||||
final currentReactions = Map<String, int>.from(getReactions(msg));
|
||||
currentReactions[reactionInfo.emoji] =
|
||||
(currentReactions[reactionInfo.emoji] ?? 0) + 1;
|
||||
updateMessage(i, currentReactions);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static List<String>? _cachedEmojis;
|
||||
|
||||
/// Combined list of all reaction emojis in fixed order.
|
||||
@@ -65,4 +109,9 @@ class ReactionHelper {
|
||||
|
||||
return ReactionInfo(targetHash: match.group(1)!, emoji: emoji);
|
||||
}
|
||||
|
||||
/// Encode a reaction message that parseReaction() can parse.
|
||||
static String encodeReaction(String hash, String emojiIndex) {
|
||||
return 'r:$hash:$emojiIndex';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// showDismissibleSnackBar shows a [SnackBar] with tap to dismiss
|
||||
// all other properties are default and optional
|
||||
void showDismissibleSnackBar(
|
||||
BuildContext context, {
|
||||
Key? key,
|
||||
required Widget content,
|
||||
Color? backgroundColor,
|
||||
double? elevation,
|
||||
EdgeInsetsGeometry? margin,
|
||||
EdgeInsetsGeometry? padding,
|
||||
double? width,
|
||||
ShapeBorder? shape,
|
||||
HitTestBehavior? hitTestBehavior,
|
||||
SnackBarBehavior? behavior,
|
||||
SnackBarAction? action,
|
||||
double? actionOverflowThreshold,
|
||||
bool? showCloseIcon,
|
||||
Color? closeIconColor,
|
||||
Duration? duration,
|
||||
bool? persist,
|
||||
Animation<double>? animation,
|
||||
void Function()? onVisible,
|
||||
DismissDirection? dismissDirection,
|
||||
Clip? clipBehavior,
|
||||
}) {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
key: key,
|
||||
content: GestureDetector(
|
||||
onTap: () => messenger.hideCurrentSnackBar(),
|
||||
child: content,
|
||||
),
|
||||
backgroundColor: backgroundColor,
|
||||
elevation: elevation,
|
||||
margin: margin,
|
||||
padding: padding,
|
||||
width: width,
|
||||
shape: shape,
|
||||
hitTestBehavior: hitTestBehavior,
|
||||
behavior: behavior,
|
||||
action: action,
|
||||
actionOverflowThreshold: actionOverflowThreshold,
|
||||
showCloseIcon: showCloseIcon,
|
||||
closeIconColor: closeIconColor,
|
||||
duration: duration ?? const Duration(seconds: 4),
|
||||
persist: persist,
|
||||
animation: animation,
|
||||
onVisible: onVisible,
|
||||
dismissDirection: dismissDirection ?? DismissDirection.down,
|
||||
clipBehavior: clipBehavior ?? Clip.hardEdge,
|
||||
),
|
||||
);
|
||||
}
|
||||
+275
-1
@@ -285,6 +285,7 @@
|
||||
"contacts_newGroup": "Нова група",
|
||||
"contacts_groupName": "Група",
|
||||
"contacts_groupNameRequired": "Името на групата е задължително.",
|
||||
"contacts_groupNameReserved": "Това име на група е запазено",
|
||||
"contacts_groupAlreadyExists": "Групата \"{name}\" вече съществува.",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -1607,6 +1608,8 @@
|
||||
"scanner_bluetoothOff": "Bluetooth е изключен.",
|
||||
"scanner_enableBluetooth": "Активирайте Bluetooth",
|
||||
"scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.",
|
||||
"scanner_chromeRequired": "Изисква се браузър Chrome",
|
||||
"scanner_chromeRequiredMessage": "Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.",
|
||||
"snrIndicator_lastSeen": "Последно видян",
|
||||
"snrIndicator_nearByRepeaters": "Близки повтарящи се устройства",
|
||||
"chat_ShowAllPaths": "Покажи всички пътища",
|
||||
@@ -1800,5 +1803,276 @@
|
||||
"contacts_unread": "Непрочетено",
|
||||
"contacts_searchRepeaters": "Търсене на {number}{str} повтарящи се...",
|
||||
"contacts_searchContactsNoNumber": "Търси контакти...",
|
||||
"contacts_searchUsers": "Търсене на {number}{str} потребители..."
|
||||
"contacts_searchUsers": "Търсене на {number}{str} потребители...",
|
||||
"contactsSettings_title": "Настройки на контактите",
|
||||
"contactsSettings_autoAddTitle": "Автоматично откриване",
|
||||
"contactsSettings_autoAddUsersTitle": "Автоматично добавяне на потребители",
|
||||
"contactsSettings_otherTitle": "Други настройки свързани с контакти",
|
||||
"settings_contactSettingsSubtitle": "Настройки за добавяне на контакти.",
|
||||
"settings_contactSettings": "Настройки за контакти",
|
||||
"contactsSettings_autoAddSensorsTitle": "Автоматично добавяне на датчици",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Автоматично добавяне на сървъри на стаите",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Позволи на спътника да добавя автоматично откритите сървъри на стаите.",
|
||||
"contactsSettings_autoAddRepeatersTitle": "Автоматично добавяне на повтарящи се елементи",
|
||||
"contactsSettings_autoAddUsersSubtitle": "Позволи на спътника да добавя автоматично откритите потребители.",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "Позволи на спътника да добавя автоматично откритите повтарящи се устройства.",
|
||||
"contactsSettings_autoAddSensorsSubtitle": "Позволи на спътника да добавя автоматично откритите датчици.",
|
||||
"contactsSettings_overwriteOldestTitle": "Премахни най-старото",
|
||||
"discoveredContacts_Title": "Открити контакти",
|
||||
"discoveredContacts_searchHint": "Търсене на открити контакти",
|
||||
"discoveredContacts_noMatching": "Няма съвпадащи контакти",
|
||||
"discoveredContacts_contactAdded": "Контакт добавен",
|
||||
"discoveredContacts_copyContact": "Копирай контакт в клипборда",
|
||||
"discoveredContacts_deleteContact": "Изтрий контакт",
|
||||
"discoveredContacts_addContact": "Добави контакт",
|
||||
"contactsSettings_overwriteOldestSubtitle": "Когато списъкът с контакти е пълен, най-старият неключов контакт ще бъде заменен.",
|
||||
"discoveredContacts_deleteContactAll": "Изтриване на Всички Открити Контакти",
|
||||
"discoveredContacts_deleteContactAllContent": "Сигурни ли сте, че искате да изтриете всички открити контакти?",
|
||||
"common_deleteAll": "Изтрий всичко",
|
||||
"map_guessedLocation": "Предполагано местоположение",
|
||||
"map_showGuessedLocations": "Покажете местоположенията на предположените възли.",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"usbScreenTitle": "Свържете се чрез USB",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"usbScreenSubtitle": "Изберете открития сериен уред и свържете директно към вашия MeshCore възел.",
|
||||
"usbScreenStatus": "Изберете USB устройство",
|
||||
"usbScreenNote": "USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.",
|
||||
"usbScreenEmptyState": "Няма открити USB устройства. Включете едно и опитайте отново.",
|
||||
"usbErrorPermissionDenied": "Не беше разрешено достъпът през USB.",
|
||||
"usbErrorDeviceMissing": "Избраното USB устройство вече не е налично.",
|
||||
"usbErrorInvalidPort": "Изберете валитно USB устройство.",
|
||||
"usbErrorBusy": "Друг мол за свързване през USB вече е в процес на изпълнение.",
|
||||
"usbErrorNotConnected": "Няма свързано USB устройство.",
|
||||
"usbErrorOpenFailed": "Не успях да отворя избраното USB устройство.",
|
||||
"usbErrorConnectFailed": "Не успях да се свържа с избраното USB устройство.",
|
||||
"usbErrorUnsupported": "USB серийната комуникация не се поддържа на тази платформа.",
|
||||
"usbErrorAlreadyActive": "USB връзката вече е активирана.",
|
||||
"usbErrorNoDeviceSelected": "Няма избран USB устройство.",
|
||||
"usbErrorPortClosed": "USB връзката не е активна.",
|
||||
"usbFallbackDeviceName": "Устройство за четене на уеб серийни данни",
|
||||
"@usbConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"usbStatus_connecting": "Свързване към USB устройство...",
|
||||
"usbConnectionFailed": "Неуспешно свързване през USB: {error}",
|
||||
"usbStatus_notConnected": "Изберете USB устройство",
|
||||
"usbStatus_searching": "Търсене на USB устройства...",
|
||||
"usbErrorConnectTimedOut": "Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка.",
|
||||
"@tcpStatus_connectingTo": {
|
||||
"placeholders": {
|
||||
"endpoint": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@tcpConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tcpHostHint": "192.168.40.10",
|
||||
"tcpScreenTitle": "Свържете се чрез TCP",
|
||||
"connectionChoiceTcpLabel": "TCP",
|
||||
"tcpHostLabel": "IP адрес",
|
||||
"tcpPortLabel": "Пристанище",
|
||||
"tcpPortHint": "5000",
|
||||
"tcpStatus_notConnected": "Въведете крайната точка и свържете се.",
|
||||
"tcpStatus_connectingTo": "Свързване към {endpoint}...",
|
||||
"tcpErrorHostRequired": "Необходим е IP адрес.",
|
||||
"tcpErrorPortInvalid": "Портът трябва да бъде между 1 и 65535.",
|
||||
"tcpErrorUnsupported": "Транспортът чрез TCP не се поддържа на тази платформа.",
|
||||
"tcpErrorTimedOut": "Връзката TCP изтекла.",
|
||||
"tcpConnectionFailed": "Неуспешно е установено TCP връзката: {error}",
|
||||
"map_showDiscoveryContacts": "Покажи контакти за откриване",
|
||||
"map_setAsMyLocation": "Задайте като моя местоположение",
|
||||
"@path_routeWeight": {
|
||||
"placeholders": {
|
||||
"weight": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": "Начално тегло за новооткрити маршрути",
|
||||
"appSettings_maxRouteWeightSubtitle": "Максималното тегло, което един маршрут може да събере от успешни доставки.",
|
||||
"appSettings_routeWeightSuccessIncrement": "Увеличение на теглото за успех",
|
||||
"appSettings_routeWeightSuccessIncrementSubtitle": "Тегло, добавено към път след успешно доставяне.",
|
||||
"appSettings_routeWeightFailureDecrement": "Намаляване на теглото, свързано с неуспех",
|
||||
"appSettings_routeWeightFailureDecrementSubtitle": "Тегло, което е било премахнато от пътя след неуспешен опит за доставка.",
|
||||
"appSettings_maxMessageRetries": "Максимален брой опити за изпращане на съобщение",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@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 и време на пренос",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_enableTitle": "Активирайте превода",
|
||||
"translation_title": "Превод",
|
||||
"translation_composerTitle": "Преведете преди да изпратите",
|
||||
"translation_enableSubtitle": "Превеждайте входящите съобщения и позволявайте предварително превеждане преди изпращане.",
|
||||
"translation_composerSubtitle": "Контролира началния статус на иконата за превод, създадена от композитора.",
|
||||
"translation_targetLanguage": "Целеви език",
|
||||
"translation_useAppLanguage": "Използвайте езика на приложението",
|
||||
"translation_downloadedModelLabel": "Изтегнат модел",
|
||||
"translation_presetModelLabel": "Предварително конфигуриран модел от Hugging Face",
|
||||
"translation_manualUrlLabel": "URL на ръководството",
|
||||
"translation_downloadModel": "Изтеглете модела",
|
||||
"translation_downloading": "Изтегляне...",
|
||||
"translation_working": "Работа...",
|
||||
"translation_stop": "Спрете",
|
||||
"translation_mergingChunks": "Съединяване на изтеглените части в един файл...",
|
||||
"translation_downloadedModels": "Изтеглени модели",
|
||||
"translation_deleteModel": "Изтриване на модела",
|
||||
"translation_modelDownloaded": "Моделът за превод е изтеглен.",
|
||||
"translation_downloadStopped": "Изтеглянето беше прекъснато.",
|
||||
"translation_downloadFailed": "Не успях да изтегля: {error}",
|
||||
"translation_enterUrlFirst": "Въведете първо URL адрес на модела.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerDisabledHint": "Изпращайте съобщения на оригиналния въведен език.",
|
||||
"translation_translateBeforeSending": "Преведете преди да изпратите",
|
||||
"translation_messageTranslation": "Превод на съобщението",
|
||||
"translation_composerEnabledHint": "Съобщенията ще бъдат преведени, преди да бъдат изпратени.",
|
||||
"translation_translateTo": "Превеждане на {language}",
|
||||
"translation_translationOptions": "Опции за превод",
|
||||
"translation_systemLanguage": "Език на системата",
|
||||
"scanner_linuxPairingPinTitle": "PIN за съвпадение чрез Bluetooth",
|
||||
"scanner_linuxPairingPinPrompt": "Въведете PIN кода за {deviceName} (оставете празно, ако няма такъв).",
|
||||
"scanner_linuxPairingHidePin": "Скриване на PIN кода",
|
||||
"scanner_linuxPairingShowPin": "Покажи PIN",
|
||||
"repeater_cliQuickClockSync": "Синхронизация на часовника",
|
||||
"repeater_cliQuickDiscovery": "Открий Съседи",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Автоматично изпращайте съобщение \"синхронизиране на часовника\" след успешно влизане.",
|
||||
"repeater_clockSyncAfterLogin": "Синхронизиране на часовника след влизане",
|
||||
"chat_sendMessage": "Изпратете съобщение",
|
||||
"room_guest": "Информация за сървъра на стаята",
|
||||
"repeater_guest": "Информация за ретранслаторите",
|
||||
"repeater_guestTools": "Инструменти за гости"
|
||||
}
|
||||
|
||||
+281
-7
@@ -285,6 +285,7 @@
|
||||
"contacts_newGroup": "Neue Gruppe",
|
||||
"contacts_groupName": "Gruppenname",
|
||||
"contacts_groupNameRequired": "Der Gruppennamen ist erforderlich.",
|
||||
"contacts_groupNameReserved": "Dieser Gruppenname ist reserviert",
|
||||
"contacts_groupAlreadyExists": "Die Gruppe \"{name}\" existiert bereits.",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -296,8 +297,8 @@
|
||||
"contacts_filterContacts": "Filtert Kontakte...",
|
||||
"contacts_noContactsMatchFilter": "Keine Kontakte passen zu Ihrem Filter",
|
||||
"contacts_noMembers": "Keine Mitglieder",
|
||||
"contacts_lastSeenNow": "gerade gesehen",
|
||||
"contacts_lastSeenMinsAgo": "Letzte Sichtung vor {minutes} Minuten.",
|
||||
"contacts_lastSeenNow": "kürzlich",
|
||||
"contacts_lastSeenMinsAgo": "~ {minutes} Min.",
|
||||
"@contacts_lastSeenMinsAgo": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
@@ -305,8 +306,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_lastSeenHourAgo": "Letzte Sichtung vor 1 Stunde.",
|
||||
"contacts_lastSeenHoursAgo": "Letzte Sichtung vor {hours} Stunden.",
|
||||
"contacts_lastSeenHourAgo": "~ 1 Std.",
|
||||
"contacts_lastSeenHoursAgo": "~ {hours} Std.",
|
||||
"@contacts_lastSeenHoursAgo": {
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
@@ -314,8 +315,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_lastSeenDayAgo": "Letzte Sichtung vor 1 Tag",
|
||||
"contacts_lastSeenDaysAgo": "Letzte Sichtung {days} Tage zuvor",
|
||||
"contacts_lastSeenDayAgo": "~ 1 Tag",
|
||||
"contacts_lastSeenDaysAgo": "~ {days} Tage",
|
||||
"@contacts_lastSeenDaysAgo": {
|
||||
"placeholders": {
|
||||
"days": {
|
||||
@@ -1636,6 +1637,8 @@
|
||||
"pathTrace_clearTooltip": "Pfad löschen",
|
||||
"map_pathTraceCancelled": "Pfadverfolgung abgebrochen.",
|
||||
"scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.",
|
||||
"scanner_chromeRequired": "Chrome Browser erforderlich",
|
||||
"scanner_chromeRequiredMessage": "Diese Webanwendung erfordert Google Chrome oder einen Chromium-basierten Browser für die Bluetooth-Unterstützung.",
|
||||
"scanner_bluetoothOff": "Bluetooth ist deaktiviert.",
|
||||
"scanner_enableBluetooth": "Bluetooth aktivieren",
|
||||
"snrIndicator_lastSeen": "Zuletzt gesehen",
|
||||
@@ -1828,5 +1831,276 @@
|
||||
"contacts_searchRepeaters": "Suche {number}{str} Repeater...",
|
||||
"contacts_searchFavorites": "Suche {number}{str} Favoriten...",
|
||||
"contacts_searchUsers": "Suche {number}{str} Benutzer...",
|
||||
"contacts_searchRoomServers": "Suche {number}{str} Raumserver..."
|
||||
"contacts_searchRoomServers": "Suche {number}{str} Raumserver...",
|
||||
"settings_contactSettings": "Kontakteinstellungen",
|
||||
"contactsSettings_otherTitle": "Weitere Einstellungen zu Kontakten",
|
||||
"contactsSettings_title": "Kontakteinstellungen",
|
||||
"contactsSettings_autoAddTitle": "Automatische Erkennung",
|
||||
"contactsSettings_autoAddUsersTitle": "Automatische Hinzufügung von Benutzern",
|
||||
"settings_contactSettingsSubtitle": "Einstellungen für das Hinzufügen von Kontakten",
|
||||
"contactsSettings_autoAddSensorsTitle": "Automatisch Sensoren hinzufügen",
|
||||
"contactsSettings_autoAddUsersSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Benutzer hinzuzufügen",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Automatisch Raumservers hinzufügen",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Ermöglichen Sie dem Begleiter, entdeckte Raumserver automatisch hinzuzufügen",
|
||||
"contactsSettings_autoAddRepeatersTitle": "Automatisch Repeater hinzufügen",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Repeater hinzuzufügen.",
|
||||
"discoveredContacts_noMatching": "Keine passenden Kontakte",
|
||||
"discoveredContacts_searchHint": "Entdeckte Kontakte suchen",
|
||||
"discoveredContacts_addContact": "Kontakt hinzufügen",
|
||||
"discoveredContacts_contactAdded": "Kontakt hinzugefügt",
|
||||
"discoveredContacts_deleteContact": "Kontakt löschen",
|
||||
"discoveredContacts_Title": "Entdeckte Kontakte",
|
||||
"discoveredContacts_copyContact": "Kontakt in die Zwischenablage kopieren",
|
||||
"contactsSettings_overwriteOldestTitle": "Überschreiben des Ältesten",
|
||||
"contactsSettings_autoAddSensorsSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Sensoren hinzuzufügen",
|
||||
"contactsSettings_overwriteOldestSubtitle": "Wenn die Kontaktliste voll ist, wird der älteste nicht favorisierte Kontakt ersetzt.",
|
||||
"common_deleteAll": "Alles löschen",
|
||||
"discoveredContacts_deleteContactAllContent": "Sind Sie sicher, dass Sie alle gefundenen Kontakte löschen möchten?",
|
||||
"discoveredContacts_deleteContactAll": "Alle entdeckten Kontakte löschen",
|
||||
"map_showGuessedLocations": "Zeige die vermuteten Knotenpositionen",
|
||||
"map_guessedLocation": "Geschätzter Ort",
|
||||
"usbScreenSubtitle": "Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"usbScreenTitle": "Verbinden über USB",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"usbScreenStatus": "Wählen Sie ein USB-Gerät aus",
|
||||
"usbScreenNote": "Die USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.",
|
||||
"usbScreenEmptyState": "Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie.",
|
||||
"usbErrorPermissionDenied": "Die USB-Berechtigung wurde abgelehnt.",
|
||||
"usbErrorDeviceMissing": "Das ausgewählte USB-Gerät ist nicht mehr verfügbar.",
|
||||
"usbErrorInvalidPort": "Wählen Sie ein gültiges USB-Gerät aus.",
|
||||
"usbErrorBusy": "Eine weitere Anfrage für eine USB-Verbindung ist bereits in Bearbeitung.",
|
||||
"usbErrorNotConnected": "Es ist kein USB-Gerät angeschlossen.",
|
||||
"usbErrorOpenFailed": "Fehlgeschlagen beim Öffnen des ausgewählten USB-Geräts.",
|
||||
"usbErrorConnectFailed": "Keine Verbindung zum ausgewählten USB-Gerät hergestellt.",
|
||||
"usbErrorUnsupported": "Die USB-Serielle Schnittstelle wird auf dieser Plattform nicht unterstützt.",
|
||||
"usbErrorAlreadyActive": "Eine USB-Verbindung ist bereits hergestellt.",
|
||||
"usbErrorNoDeviceSelected": "Kein USB-Gerät wurde ausgewählt.",
|
||||
"usbErrorPortClosed": "Die USB-Verbindung ist nicht aktiv.",
|
||||
"usbFallbackDeviceName": "Web-Serielle Geräte",
|
||||
"@usbConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"usbStatus_searching": "Suche nach USB-Geräten...",
|
||||
"usbStatus_notConnected": "Wählen Sie ein USB-Gerät aus",
|
||||
"usbStatus_connecting": "Verbindung zum USB-Gerät...",
|
||||
"usbConnectionFailed": "Fehler beim USB-Verbindungsaufbau: {error}",
|
||||
"usbErrorConnectTimedOut": "Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält.",
|
||||
"@tcpStatus_connectingTo": {
|
||||
"placeholders": {
|
||||
"endpoint": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@tcpConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tcpHostLabel": "IP-Adresse",
|
||||
"connectionChoiceTcpLabel": "TCP",
|
||||
"tcpHostHint": "192.168.40.10",
|
||||
"tcpScreenTitle": "Verbinden über TCP",
|
||||
"tcpPortLabel": "Port",
|
||||
"tcpPortHint": "5000",
|
||||
"tcpStatus_notConnected": "Geben Sie den Endpunkt ein und verbinden Sie sich.",
|
||||
"tcpStatus_connectingTo": "Verbindung zu {endpoint}...",
|
||||
"tcpErrorHostRequired": "Eine IP-Adresse ist erforderlich.",
|
||||
"tcpErrorPortInvalid": "Die Portnummer muss zwischen 1 und 65535 liegen.",
|
||||
"tcpErrorUnsupported": "Die TCP-Übertragung wird auf dieser Plattform nicht unterstützt.",
|
||||
"tcpErrorTimedOut": "Die TCP-Verbindung ist abgelaufen.",
|
||||
"tcpConnectionFailed": "Fehler beim TCP-Verbindungsaufbau: {error}",
|
||||
"map_showDiscoveryContacts": "Entdeckungs-Kontakte anzeigen",
|
||||
"map_setAsMyLocation": "Als meine aktuelle Position festlegen",
|
||||
"@path_routeWeight": {
|
||||
"placeholders": {
|
||||
"weight": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"appSettings_initialRouteWeight": "Anfangs-Streckengewicht",
|
||||
"appSettings_routeWeightSuccessIncrement": "Erhöhung des Erfolgsgewichts",
|
||||
"appSettings_routeWeightSuccessIncrementSubtitle": "Gewicht, das einem Pfad nach erfolgreicher Lieferung hinzugefügt wird.",
|
||||
"appSettings_routeWeightFailureDecrement": "Reduzierung des Gewichts bei Fehlern",
|
||||
"appSettings_routeWeightFailureDecrementSubtitle": "Gewicht, das nach einem fehlgeschlagenen Versand von einem Weg entfernt wurde",
|
||||
"appSettings_maxMessageRetries": "Maximale Anzahl an Wiederholungsversuchen",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@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",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_title": "Übersetzung",
|
||||
"translation_composerTitle": "Übersetzen Sie vor dem Versenden",
|
||||
"translation_enableSubtitle": "Nachrichten empfangen und übersetzen sowie die Möglichkeit bieten, Nachrichten vor dem Versenden zu übersetzen.",
|
||||
"translation_enableTitle": "Aktivieren Sie die Übersetzung",
|
||||
"translation_composerSubtitle": "Steuert den Standardzustand des Icons für die Übersetzung des Komponisten.",
|
||||
"translation_targetLanguage": "Zielsprache",
|
||||
"translation_useAppLanguage": "Verwenden Sie die App-Sprache",
|
||||
"translation_downloadedModelLabel": "Heruntergeladenes Modell",
|
||||
"translation_presetModelLabel": "Vordefinierter Hugging Face-Modell",
|
||||
"translation_manualUrlLabel": "URL für das manuelle Modell",
|
||||
"translation_downloadModel": "Modell herunterladen",
|
||||
"translation_downloading": "Herunterladen...",
|
||||
"translation_working": "Arbeiten...",
|
||||
"translation_stop": "Stopp",
|
||||
"translation_mergingChunks": "Zusammenführen der heruntergeladenen Teile in die finale Datei...",
|
||||
"translation_downloadedModels": "Heruntergeladene Modelle",
|
||||
"translation_deleteModel": "Modell löschen",
|
||||
"translation_modelDownloaded": "Übersetzungsmotor heruntergeladen.",
|
||||
"translation_downloadStopped": "Herunterladen abgebrochen.",
|
||||
"translation_downloadFailed": "Download fehlgeschlagen: {error}",
|
||||
"translation_enterUrlFirst": "Geben Sie zunächst die URL eines Modells ein.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_messageTranslation": "Nachricht übersetzen",
|
||||
"translation_composerEnabledHint": "Die Nachrichten werden vor dem Versenden übersetzt.",
|
||||
"translation_translateBeforeSending": "Übersetzen Sie vor dem Versenden",
|
||||
"translation_composerDisabledHint": "Nachrichten in der ursprünglichen, getippten Sprache senden.",
|
||||
"translation_translateTo": "Übersetzen Sie auf {language}",
|
||||
"translation_translationOptions": "Übersetzungsmöglichkeiten",
|
||||
"translation_systemLanguage": "Sprache des Systems",
|
||||
"scanner_linuxPairingShowPin": "PIN anzeigen",
|
||||
"scanner_linuxPairingHidePin": "PIN ausblenden",
|
||||
"scanner_linuxPairingPinTitle": "Bluetooth-Paarungs-PIN",
|
||||
"scanner_linuxPairingPinPrompt": "Geben Sie die PIN für {deviceName} ein (leer lassen, falls keine).",
|
||||
"repeater_cliQuickClockSync": "Uhr Synchronisieren",
|
||||
"repeater_cliQuickDiscovery": "Entdecke Nachbarn",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLogin": "Uhrzeit-Synchronisation nach dem Anmelden",
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Automatisch \"Uhrzeit-Synchronisierung\" nach erfolgreicher Anmeldung senden.",
|
||||
"repeater_guest": "Informationen zu Repeatern",
|
||||
"repeater_guestTools": "Gastwerkzeuge",
|
||||
"chat_sendMessage": "Nachricht senden",
|
||||
"room_guest": "Informationen zum Room Server"
|
||||
}
|
||||
|
||||
+294
-10
@@ -10,6 +10,7 @@
|
||||
"common_unknownDevice": "Unknown Device",
|
||||
"common_save": "Save",
|
||||
"common_delete": "Delete",
|
||||
"common_deleteAll": "Delete All",
|
||||
"common_close": "Close",
|
||||
"common_edit": "Edit",
|
||||
"common_add": "Add",
|
||||
@@ -46,6 +47,64 @@
|
||||
}
|
||||
},
|
||||
"scanner_title": "MeshCore Open",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"connectionChoiceTcpLabel": "TCP",
|
||||
"tcpScreenTitle": "Connect over TCP",
|
||||
"tcpHostLabel": "IP Address",
|
||||
"tcpHostHint": "192.168.40.10",
|
||||
"tcpPortLabel": "Port",
|
||||
"tcpPortHint": "5000",
|
||||
"tcpStatus_notConnected": "Enter endpoint and connect",
|
||||
"tcpStatus_connectingTo": "Connecting to {endpoint}...",
|
||||
"@tcpStatus_connectingTo": {
|
||||
"placeholders": {
|
||||
"endpoint": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tcpErrorHostRequired": "IP address is required.",
|
||||
"tcpErrorPortInvalid": "Port must be between 1 and 65535.",
|
||||
"tcpErrorUnsupported": "TCP transport is not supported on this platform.",
|
||||
"tcpErrorTimedOut": "TCP connection timed out.",
|
||||
"tcpConnectionFailed": "TCP connection failed: {error}",
|
||||
"@tcpConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"usbScreenTitle": "Connect over USB",
|
||||
"usbScreenSubtitle": "Choose a detected serial device and connect directly to your MeshCore node.",
|
||||
"usbScreenStatus": "Select a USB device",
|
||||
"usbScreenNote": "USB serial is active on supported Android devices and desktop platforms.",
|
||||
"usbScreenEmptyState": "No USB devices found. Plug one in and refresh.",
|
||||
"usbErrorPermissionDenied": "USB permission was denied.",
|
||||
"usbErrorDeviceMissing": "The selected USB device is no longer available.",
|
||||
"usbErrorInvalidPort": "Select a valid USB device.",
|
||||
"usbErrorBusy": "Another USB connection request is already in progress.",
|
||||
"usbErrorNotConnected": "No USB device is connected.",
|
||||
"usbErrorOpenFailed": "Failed to open the selected USB device.",
|
||||
"usbErrorConnectFailed": "Failed to connect to the selected USB device.",
|
||||
"usbErrorUnsupported": "USB serial is not supported on this platform.",
|
||||
"usbErrorAlreadyActive": "A USB connection is already active.",
|
||||
"usbErrorNoDeviceSelected": "No USB device was selected.",
|
||||
"usbErrorPortClosed": "The USB connection is not open.",
|
||||
"usbErrorConnectTimedOut": "Connection timed out. Make sure the device has USB Companion firmware.",
|
||||
"usbFallbackDeviceName": "Web Serial Device",
|
||||
"usbStatus_notConnected": "Select a USB device",
|
||||
"usbStatus_connecting": "Connecting to USB device...",
|
||||
"usbStatus_searching": "Searching for USB devices...",
|
||||
"usbConnectionFailed": "USB connection failed: {error}",
|
||||
"@usbConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner_scanning": "Scanning for devices...",
|
||||
"scanner_connecting": "Connecting...",
|
||||
"scanner_disconnecting": "Disconnecting...",
|
||||
@@ -68,10 +127,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"scanner_stop": "Stop",
|
||||
"scanner_scan": "Scan",
|
||||
"scanner_bluetoothOff": "Bluetooth is off",
|
||||
"scanner_bluetoothOffMessage": "Please turn on Bluetooth to scan for devices",
|
||||
"scanner_chromeRequired": "Chrome Browser Required",
|
||||
"scanner_chromeRequiredMessage": "This web application requires Google Chrome or a Chromium-based browser for Bluetooth support.",
|
||||
"scanner_enableBluetooth": "Enable Bluetooth",
|
||||
"device_quickSwitch": "Quick switch",
|
||||
"device_meshcore": "MeshCore",
|
||||
@@ -98,11 +160,33 @@
|
||||
"settings_locationIntervalInvalid": "Interval must be at least 60 seconds, and less than 86400 seconds.",
|
||||
"settings_latitude": "Latitude",
|
||||
"settings_longitude": "Longitude",
|
||||
"settings_contactSettings": "Contact Settings",
|
||||
"settings_contactSettingsSubtitle": "Settings for how contacts are added.",
|
||||
"settings_privacyMode": "Privacy Mode",
|
||||
"settings_privacyModeSubtitle": "Hide name/location in advertisements",
|
||||
"settings_privacyModeToggle": "Toggle privacy mode to hide your name and location in advertisements.",
|
||||
"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",
|
||||
@@ -206,6 +290,27 @@
|
||||
"appSettings_autoRouteRotationSubtitle": "Cycle between best paths and flood mode",
|
||||
"appSettings_autoRouteRotationEnabled": "Auto route rotation enabled",
|
||||
"appSettings_autoRouteRotationDisabled": "Auto route rotation disabled",
|
||||
"appSettings_maxRouteWeight": "Max Route Weight",
|
||||
"appSettings_maxRouteWeightSubtitle": "Maximum weight a path can accumulate from successful deliveries",
|
||||
"appSettings_initialRouteWeight": "Initial Route Weight",
|
||||
"appSettings_initialRouteWeightSubtitle": "Starting weight for newly discovered paths",
|
||||
"appSettings_routeWeightSuccessIncrement": "Success Weight Increment",
|
||||
"appSettings_routeWeightSuccessIncrementSubtitle": "Weight added to a path after successful delivery",
|
||||
"appSettings_routeWeightFailureDecrement": "Failure Weight Decrement",
|
||||
"appSettings_routeWeightFailureDecrementSubtitle": "Weight removed from a path after failed delivery",
|
||||
"appSettings_maxMessageRetries": "Max Message Retries",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Number of retry attempts before marking a message as failed",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"@path_routeWeight": {
|
||||
"placeholders": {
|
||||
"weight": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_battery": "Battery",
|
||||
"appSettings_batteryChemistry": "Battery Chemistry",
|
||||
"appSettings_batteryChemistryPerDevice": "Set per device ({deviceName})",
|
||||
@@ -353,6 +458,7 @@
|
||||
"contacts_newGroup": "New Group",
|
||||
"contacts_groupName": "Group name",
|
||||
"contacts_groupNameRequired": "Group name is required",
|
||||
"contacts_groupNameReserved": "This group name is reserved",
|
||||
"contacts_groupAlreadyExists": "Group \"{name}\" already exists",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -364,8 +470,8 @@
|
||||
"contacts_filterContacts": "Filter contacts...",
|
||||
"contacts_noContactsMatchFilter": "No contacts match your filter",
|
||||
"contacts_noMembers": "No members",
|
||||
"contacts_lastSeenNow": "Last seen now",
|
||||
"contacts_lastSeenMinsAgo": "Last seen {minutes} mins ago",
|
||||
"contacts_lastSeenNow": "recently",
|
||||
"contacts_lastSeenMinsAgo": "~ {minutes} min.",
|
||||
"@contacts_lastSeenMinsAgo": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
@@ -373,8 +479,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_lastSeenHourAgo": "Last seen 1 hour ago",
|
||||
"contacts_lastSeenHoursAgo": "Last seen {hours} hours ago",
|
||||
"contacts_lastSeenHourAgo": "~ 1 hour",
|
||||
"contacts_lastSeenHoursAgo": "~ {hours} hours",
|
||||
"@contacts_lastSeenHoursAgo": {
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
@@ -382,8 +488,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_lastSeenDayAgo": "Last seen 1 day ago",
|
||||
"contacts_lastSeenDaysAgo": "Last seen {days} days ago",
|
||||
"contacts_lastSeenDayAgo": "~ 1 day",
|
||||
"contacts_lastSeenDaysAgo": "~ {days} days",
|
||||
"@contacts_lastSeenDaysAgo": {
|
||||
"placeholders": {
|
||||
"days": {
|
||||
@@ -391,6 +497,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",
|
||||
@@ -490,6 +607,15 @@
|
||||
"channels_enterHashtag": "Enter hashtag",
|
||||
"channels_hashtagHint": "e.g. #team",
|
||||
"chat_noMessages": "No messages yet",
|
||||
"chat_sendMessage": "Send message",
|
||||
"chat_sendMessageTo": "Send message to {name}",
|
||||
"@chat_sendMessageTo": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_sendMessageToStart": "Send a message to get started",
|
||||
"chat_originalMessageNotFound": "Original message not found",
|
||||
"chat_replyingTo": "Replying to {name}",
|
||||
@@ -744,6 +870,7 @@
|
||||
"map_source": "Source",
|
||||
"map_flags": "Flags",
|
||||
"map_shareMarkerHere": "Share marker here",
|
||||
"map_setAsMyLocation": "Set as my location",
|
||||
"map_pinLabel": "Pin label",
|
||||
"map_label": "Label",
|
||||
"map_pointOfInterest": "Point of interest",
|
||||
@@ -765,17 +892,22 @@
|
||||
"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",
|
||||
"map_markers": "Markers",
|
||||
"map_showSharedMarkers": "Show shared markers",
|
||||
"map_showGuessedLocations": "Show guessed node locations",
|
||||
"map_showDiscoveryContacts": "Show Discovery Contacts",
|
||||
"map_guessedLocation": "Guessed location",
|
||||
"map_lastSeenTime": "Last Seen Time",
|
||||
"map_sharedPin": "Shared pin",
|
||||
"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",
|
||||
@@ -906,8 +1038,8 @@
|
||||
"login_enterPassword": "Enter password",
|
||||
"login_savePassword": "Save password",
|
||||
"login_savePasswordSubtitle": "Password will be stored securely on this device",
|
||||
"login_repeaterDescription": "Enter the repeater password to access settings and status.",
|
||||
"login_roomDescription": "Enter the room password to access settings and status.",
|
||||
"login_repeaterDescription": "Enter the repeater password for guest or admin access.",
|
||||
"login_roomDescription": "Enter the room password for guest or admin access.",
|
||||
"login_routing": "Routing",
|
||||
"login_routingMode": "Routing mode",
|
||||
"login_autoUseSavedPath": "Auto (use saved path)",
|
||||
@@ -973,7 +1105,10 @@
|
||||
"path_setPath": "Set Path",
|
||||
"repeater_management": "Repeater Management",
|
||||
"room_management": "Room Server Management",
|
||||
"repeater_guest": "Repeater Information",
|
||||
"room_guest": "Room Server Information",
|
||||
"repeater_managementTools": "Management Tools",
|
||||
"repeater_guestTools": "Guest Tools",
|
||||
"repeater_status": "Status",
|
||||
"repeater_statusSubtitle": "View repeater status, stats, and neighbors",
|
||||
"repeater_telemetry": "Telemetry",
|
||||
@@ -984,6 +1119,14 @@
|
||||
"repeater_neighborsSubtitle": "View zero hop neighbors.",
|
||||
"repeater_settings": "Settings",
|
||||
"repeater_settingsSubtitle": "Configure repeater parameters",
|
||||
"repeater_clockSyncAfterLogin": "Clock sync after login",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Automatically send \"clock sync\" after a successful login",
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_statusTitle": "Repeater Status",
|
||||
"repeater_routingMode": "Routing mode",
|
||||
"repeater_autoUseSavedPath": "Auto (use saved path)",
|
||||
@@ -1214,6 +1357,8 @@
|
||||
"repeater_cliQuickVersion": "Version",
|
||||
"repeater_cliQuickAdvertise": "Advertise",
|
||||
"repeater_cliQuickClock": "Clock",
|
||||
"repeater_cliQuickClockSync": "Clock Sync",
|
||||
"repeater_cliQuickDiscovery": "Discover Neighbors",
|
||||
"repeater_cliHelpAdvert": "Sends an advertisement packet",
|
||||
"repeater_cliHelpReboot": "Reboots the device. (note, you'll prob get 'Timeout' which is normal)",
|
||||
"repeater_cliHelpClock": "Displays current time per device's clock.",
|
||||
@@ -1838,5 +1983,144 @@
|
||||
"settings_gpxExportShareText": "Map data exported from meshcore-open",
|
||||
"settings_gpxExportShareSubject": "meshcore-open GPX map data export",
|
||||
"snrIndicator_nearByRepeaters": "Nearby Repeaters",
|
||||
"snrIndicator_lastSeen": "Last seen"
|
||||
"snrIndicator_lastSeen": "Last seen",
|
||||
"contactsSettings_title": "Contacts settings",
|
||||
"contactsSettings_autoAddTitle": "Automatic Discovery",
|
||||
"contactsSettings_otherTitle": "Other contact related settings",
|
||||
"contactsSettings_autoAddUsersTitle": "Auto-add users",
|
||||
"contactsSettings_autoAddUsersSubtitle": "Allow the companion to automatically add discovered users.",
|
||||
"contactsSettings_autoAddRepeatersTitle": "Auto-add repeaters",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "Allow the companion to automatically add discovered repeaters.",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Auto-add room servers",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Allow the companion to automatically add discovered room servers.",
|
||||
"contactsSettings_autoAddSensorsTitle": "Auto-add sensors",
|
||||
"contactsSettings_autoAddSensorsSubtitle": "Allow the companion to automatically add discovered sensors.",
|
||||
"contactsSettings_overwriteOldestTitle": "Overwrite Oldest",
|
||||
"contactsSettings_overwriteOldestSubtitle": "When the contact list is full, the oldest non-favorited contact will be replaced.",
|
||||
"discoveredContacts_Title": "Discovered Contacts",
|
||||
"discoveredContacts_noMatching": "No matching contacts",
|
||||
"discoveredContacts_searchHint": "Search discovered contacts",
|
||||
"discoveredContacts_contactAdded": "Contact added",
|
||||
"discoveredContacts_addContact": "Add Contact",
|
||||
"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?",
|
||||
"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",
|
||||
|
||||
"translation_title": "Translation",
|
||||
"translation_enableTitle": "Enable translation",
|
||||
"translation_enableSubtitle": "Translate incoming messages and allow pre-send translation.",
|
||||
"translation_composerTitle": "Translate before sending",
|
||||
"translation_composerSubtitle": "Controls the default state of the composer translation icon.",
|
||||
"translation_targetLanguage": "Target language",
|
||||
"translation_useAppLanguage": "Use app language",
|
||||
"translation_downloadedModelLabel": "Downloaded model",
|
||||
"translation_presetModelLabel": "Preset Hugging Face model",
|
||||
"translation_manualUrlLabel": "Manual model URL",
|
||||
"translation_downloadModel": "Download model",
|
||||
"translation_downloading": "Downloading...",
|
||||
"translation_working": "Working...",
|
||||
"translation_stop": "Stop",
|
||||
"translation_mergingChunks": "Merging downloaded chunks into final file...",
|
||||
"translation_downloadedModels": "Downloaded models",
|
||||
"translation_deleteModel": "Delete model",
|
||||
"translation_modelDownloaded": "Translation model downloaded.",
|
||||
"translation_downloadStopped": "Download stopped.",
|
||||
"translation_downloadFailed": "Download failed: {error}",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_enterUrlFirst": "Enter a model URL first.",
|
||||
"scanner_linuxPairingShowPin": "Show PIN",
|
||||
"scanner_linuxPairingHidePin": "Hide PIN",
|
||||
"scanner_linuxPairingPinTitle": "Bluetooth Pairing PIN",
|
||||
"scanner_linuxPairingPinPrompt": "Enter PIN for {deviceName} (leave blank if none).",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_messageTranslation": "Message translation",
|
||||
"translation_translateBeforeSending": "Translate before sending",
|
||||
"translation_composerEnabledHint": "Messages will be translated before send.",
|
||||
"translation_composerDisabledHint": "Send messages in the original typed language.",
|
||||
"translation_translateTo": "Translate to {language}",
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_translationOptions": "Translation options",
|
||||
"translation_systemLanguage": "System language"
|
||||
}
|
||||
|
||||
+280
-6
@@ -285,6 +285,7 @@
|
||||
"contacts_newGroup": "Nuevo Grupo",
|
||||
"contacts_groupName": "Nombre del grupo",
|
||||
"contacts_groupNameRequired": "El nombre del grupo es obligatorio",
|
||||
"contacts_groupNameReserved": "Este nombre de grupo está reservado",
|
||||
"contacts_groupAlreadyExists": "El grupo \"{name}\" ya existe",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -297,7 +298,7 @@
|
||||
"contacts_noContactsMatchFilter": "No hay contactos que coincidan con tu filtro",
|
||||
"contacts_noMembers": "No miembros",
|
||||
"contacts_lastSeenNow": "Última vez que se vio ahora",
|
||||
"contacts_lastSeenMinsAgo": "Última vez visto hace {minutes} minutos.",
|
||||
"contacts_lastSeenMinsAgo": "~ {minutes} min.",
|
||||
"@contacts_lastSeenMinsAgo": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
@@ -305,8 +306,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_lastSeenHourAgo": "Última vez que se vio hace 1 hora",
|
||||
"contacts_lastSeenHoursAgo": "Última vez visto hace {hours} horas.",
|
||||
"contacts_lastSeenHourAgo": "~ 1 hora",
|
||||
"contacts_lastSeenHoursAgo": "~ {hours} horas",
|
||||
"@contacts_lastSeenHoursAgo": {
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
@@ -314,8 +315,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_lastSeenDayAgo": "Última vez que se vio hace 1 día",
|
||||
"contacts_lastSeenDaysAgo": "Última vez visto hace {days} días.",
|
||||
"contacts_lastSeenDayAgo": "~ 1 día",
|
||||
"contacts_lastSeenDaysAgo": "~ {days} días",
|
||||
"@contacts_lastSeenDaysAgo": {
|
||||
"placeholders": {
|
||||
"days": {
|
||||
@@ -1633,6 +1634,8 @@
|
||||
"map_removeLast": "Eliminar último",
|
||||
"map_pathTraceCancelled": "Rastreo de ruta cancelado.",
|
||||
"scanner_bluetoothOffMessage": "Por favor, active el Bluetooth para escanear dispositivos.",
|
||||
"scanner_chromeRequired": "Navegador Chrome requerido",
|
||||
"scanner_chromeRequiredMessage": "Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.",
|
||||
"scanner_bluetoothOff": "Bluetooth está desactivado.",
|
||||
"scanner_enableBluetooth": "Habilitar Bluetooth",
|
||||
"snrIndicator_nearByRepeaters": "Repetidores cercanos",
|
||||
@@ -1828,5 +1831,276 @@
|
||||
"contacts_searchFavorites": "Buscar {number}{str} Favoritos...",
|
||||
"contacts_searchUsers": "Buscar {number}{str} Usuarios...",
|
||||
"contacts_searchRepeaters": "Buscar {number}{str} Repetidores...",
|
||||
"contacts_searchRoomServers": "Buscar {number}{str} servidores de sala..."
|
||||
"contacts_searchRoomServers": "Buscar {number}{str} servidores de sala...",
|
||||
"contactsSettings_autoAddTitle": "Detección automática",
|
||||
"settings_contactSettings": "Configuración de contacto",
|
||||
"contactsSettings_autoAddUsersTitle": "Agregar usuarios automáticamente",
|
||||
"contactsSettings_otherTitle": "Otras configuraciones relacionadas con el contacto",
|
||||
"contactsSettings_autoAddUsersSubtitle": "Permitir que el compañero agregue automáticamente a los usuarios descubiertos.",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "Permitir que el compañero agregue automáticamente los repetidores descubiertos.",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Permitir que el compañero agregue automáticamente los servidores de salas descubiertos.",
|
||||
"contactsSettings_autoAddSensorsTitle": "Agregar sensores automáticamente",
|
||||
"contactsSettings_title": "Configuración de contactos",
|
||||
"settings_contactSettingsSubtitle": "Configuración de cómo se agregan los contactos.",
|
||||
"contactsSettings_autoAddSensorsSubtitle": "Permitir que el compañero agregue automáticamente los sensores descubiertos.",
|
||||
"contactsSettings_autoAddRepeatersTitle": "Agregar repetidores automáticamente",
|
||||
"contactsSettings_overwriteOldestTitle": "Sobreescribir el más antiguo",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Agregar automáticamente servidores de sala",
|
||||
"discoveredContacts_noMatching": "No se encontraron contactos coincidentes",
|
||||
"discoveredContacts_contactAdded": "Contacto agregado",
|
||||
"discoveredContacts_copyContact": "Copiar contacto al portapapeles",
|
||||
"discoveredContacts_deleteContact": "Eliminar contacto",
|
||||
"discoveredContacts_Title": "Contactos descubiertos",
|
||||
"discoveredContacts_searchHint": "Buscar contactos descubiertos",
|
||||
"discoveredContacts_addContact": "Agregar contacto",
|
||||
"contactsSettings_overwriteOldestSubtitle": "Cuando la lista de contactos esté llena, se reemplazará el contacto no favorito más antiguo.",
|
||||
"common_deleteAll": "Eliminar todo",
|
||||
"discoveredContacts_deleteContactAll": "Eliminar Todos los Contactos Descubiertos",
|
||||
"discoveredContacts_deleteContactAllContent": "¿Está seguro de que desea eliminar todos los contactos descubiertos!",
|
||||
"map_guessedLocation": "Ubicación estimada",
|
||||
"map_showGuessedLocations": "Mostrar las ubicaciones estimadas de los nodos.",
|
||||
"usbScreenTitle": "Conecte mediante USB",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"usbScreenSubtitle": "Seleccione el dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.",
|
||||
"usbScreenStatus": "Seleccione un dispositivo USB",
|
||||
"usbScreenNote": "La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.",
|
||||
"usbScreenEmptyState": "No se encontraron dispositivos USB. Conecte uno y vuelva a intentar.",
|
||||
"usbErrorPermissionDenied": "Se denegó el permiso de acceso a través de USB.",
|
||||
"usbErrorDeviceMissing": "El dispositivo USB seleccionado ya no está disponible.",
|
||||
"usbErrorInvalidPort": "Seleccione un dispositivo USB válido.",
|
||||
"usbErrorBusy": "Ya se ha iniciado una solicitud de conexión USB adicional.",
|
||||
"usbErrorNotConnected": "No hay ningún dispositivo USB conectado.",
|
||||
"usbErrorOpenFailed": "No se pudo abrir el dispositivo USB seleccionado.",
|
||||
"usbErrorConnectFailed": "No se pudo conectar con el dispositivo USB seleccionado.",
|
||||
"usbErrorUnsupported": "La comunicación serial a través de USB no está soportada en esta plataforma.",
|
||||
"usbErrorAlreadyActive": "La conexión USB ya está activa.",
|
||||
"usbErrorNoDeviceSelected": "No se ha seleccionado ningún dispositivo USB.",
|
||||
"usbErrorPortClosed": "La conexión USB no está activa.",
|
||||
"usbFallbackDeviceName": "Dispositivo de serie web",
|
||||
"@usbConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"usbStatus_connecting": "Conectándose al dispositivo USB...",
|
||||
"usbStatus_searching": "Buscando dispositivos USB...",
|
||||
"usbStatus_notConnected": "Seleccione un dispositivo USB",
|
||||
"usbConnectionFailed": "Error al conectar mediante USB: {error}",
|
||||
"usbErrorConnectTimedOut": "La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion.",
|
||||
"@tcpStatus_connectingTo": {
|
||||
"placeholders": {
|
||||
"endpoint": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@tcpConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tcpScreenTitle": "Establecer conexión a través de TCP",
|
||||
"connectionChoiceTcpLabel": "TCP",
|
||||
"tcpHostHint": "192.168.40.10",
|
||||
"tcpHostLabel": "Dirección IP",
|
||||
"tcpPortLabel": "Puerto",
|
||||
"tcpPortHint": "5000",
|
||||
"tcpStatus_notConnected": "Ingrese la dirección final y conecte.",
|
||||
"tcpStatus_connectingTo": "Conectándose a {endpoint}...",
|
||||
"tcpErrorHostRequired": "Se requiere la dirección IP.",
|
||||
"tcpErrorPortInvalid": "El puerto debe estar entre 1 y 65535.",
|
||||
"tcpErrorUnsupported": "El protocolo de transporte TCP no está soportado en esta plataforma.",
|
||||
"tcpErrorTimedOut": "La conexión TCP ha caducado.",
|
||||
"tcpConnectionFailed": "Error en la conexión TCP: {error}",
|
||||
"map_showDiscoveryContacts": "Mostrar Contactos de Descubrimiento",
|
||||
"map_setAsMyLocation": "Establecer mi ubicación",
|
||||
"@path_routeWeight": {
|
||||
"placeholders": {
|
||||
"weight": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"appSettings_maxRouteWeightSubtitle": "Peso máximo que una ruta puede acumular gracias a entregas exitosas.",
|
||||
"appSettings_routeWeightSuccessIncrement": "Incremento de peso para el éxito",
|
||||
"appSettings_routeWeightSuccessIncrementSubtitle": "Peso añadido a una ruta después de una entrega exitosa.",
|
||||
"appSettings_routeWeightFailureDecrement": "Reducción del peso asociado al fallo",
|
||||
"appSettings_routeWeightFailureDecrementSubtitle": "Peso retirado de un camino después de un intento de entrega fallido.",
|
||||
"appSettings_maxMessageRetries": "Número máximo de reintentos de envío de mensajes",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Número de intentos de reintento antes de marcar un mensaje como fallido.",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@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",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_title": "Traducción",
|
||||
"translation_enableSubtitle": "Traducir los mensajes entrantes y permitir la traducción previa al envío.",
|
||||
"translation_enableTitle": "Habilitar la traducción",
|
||||
"translation_composerTitle": "Traducir antes de enviar",
|
||||
"translation_composerSubtitle": "Controla el estado predeterminado del icono de traducción del compositor.",
|
||||
"translation_targetLanguage": "Idioma de destino",
|
||||
"translation_useAppLanguage": "Utilizar el idioma de la aplicación",
|
||||
"translation_downloadedModelLabel": "Modelo descargado",
|
||||
"translation_presetModelLabel": "Modelo predefinido de Hugging Face",
|
||||
"translation_manualUrlLabel": "URL del modelo manual",
|
||||
"translation_downloadModel": "Descargar el modelo",
|
||||
"translation_downloading": "Descargando...",
|
||||
"translation_working": "Trabajando...",
|
||||
"translation_stop": "¡Detente!",
|
||||
"translation_mergingChunks": "Combinando los fragmentos descargados en el archivo final...",
|
||||
"translation_downloadedModels": "Modelos descargados",
|
||||
"translation_deleteModel": "Eliminar modelo",
|
||||
"translation_modelDownloaded": "Modelo de traducción descargado.",
|
||||
"translation_downloadStopped": "La descarga se ha detenido.",
|
||||
"translation_downloadFailed": "No se pudo descargar: {error}",
|
||||
"translation_enterUrlFirst": "Primero, introduzca la URL del modelo.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner_linuxPairingPinPrompt": "Introduzca el código PIN para {deviceName} (deje en blanco si no hay ninguno).",
|
||||
"scanner_linuxPairingShowPin": "Mostrar código PIN",
|
||||
"scanner_linuxPairingPinTitle": "PIN para emparejar dispositivos Bluetooth",
|
||||
"scanner_linuxPairingHidePin": "Ocultar PIN",
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerDisabledHint": "Envía mensajes utilizando el lenguaje escrito original.",
|
||||
"translation_composerEnabledHint": "Los mensajes serán traducidos antes de ser enviados.",
|
||||
"translation_messageTranslation": "Traducción del mensaje",
|
||||
"translation_translateBeforeSending": "Traducir antes de enviar",
|
||||
"translation_translateTo": "Traducir a {language}",
|
||||
"translation_translationOptions": "Opciones de traducción",
|
||||
"translation_systemLanguage": "Idioma del sistema",
|
||||
"repeater_cliQuickDiscovery": "Descubrir Vecinos",
|
||||
"repeater_cliQuickClockSync": "Sincronización del reloj",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Enviar automáticamente la función de \"sincronización de reloj\" después de un inicio de sesión exitoso.",
|
||||
"repeater_clockSyncAfterLogin": "Sincronización del reloj después de iniciar sesión",
|
||||
"repeater_guest": "Información sobre repetidores",
|
||||
"chat_sendMessage": "Enviar mensaje",
|
||||
"repeater_guestTools": "Herramientas para invitados",
|
||||
"room_guest": "Información del servidor"
|
||||
}
|
||||
|
||||
+301
-27
@@ -143,8 +143,8 @@
|
||||
"settings_frequencyHelper": "300,0 - 2 500,0",
|
||||
"settings_frequencyInvalid": "Fréquence invalide (300-2500 MHz)",
|
||||
"settings_bandwidth": "Bande passante",
|
||||
"settings_spreadingFactor": "Facteur de répartition",
|
||||
"settings_codingRate": "Taux de codage",
|
||||
"settings_spreadingFactor": "Facteur de répartition (SF)",
|
||||
"settings_codingRate": "Taux de codage (CR)",
|
||||
"settings_txPower": "TX Puissance (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Puissance TX invalide (0-22 dBm)",
|
||||
@@ -285,6 +285,7 @@
|
||||
"contacts_newGroup": "Nouveau Groupe",
|
||||
"contacts_groupName": "Nom du groupe",
|
||||
"contacts_groupNameRequired": "Le nom du groupe est obligatoire.",
|
||||
"contacts_groupNameReserved": "Ce nom de groupe est réservé",
|
||||
"contacts_groupAlreadyExists": "Le groupe \"{name}\" existe déjà.",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -297,7 +298,7 @@
|
||||
"contacts_noContactsMatchFilter": "Aucun contact ne correspond à votre filtre.",
|
||||
"contacts_noMembers": "Aucun membre",
|
||||
"contacts_lastSeenNow": "Vu maintenant",
|
||||
"contacts_lastSeenMinsAgo": "Vu il y a {minutes} minutes",
|
||||
"contacts_lastSeenMinsAgo": "~ {minutes} min.",
|
||||
"@contacts_lastSeenMinsAgo": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
@@ -305,8 +306,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_lastSeenHourAgo": "Vu il y a 1 heure",
|
||||
"contacts_lastSeenHoursAgo": "Vu il y a {hours} heures",
|
||||
"contacts_lastSeenHourAgo": "~ 1 heure",
|
||||
"contacts_lastSeenHoursAgo": "~ {hours} heures",
|
||||
"@contacts_lastSeenHoursAgo": {
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
@@ -314,8 +315,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_lastSeenDayAgo": "Vu il y a 1 jour",
|
||||
"contacts_lastSeenDaysAgo": "Vu il y a {days} jours",
|
||||
"contacts_lastSeenDayAgo": "~ 1 jour",
|
||||
"contacts_lastSeenDaysAgo": "~ {days} jours",
|
||||
"@contacts_lastSeenDaysAgo": {
|
||||
"placeholders": {
|
||||
"days": {
|
||||
@@ -566,7 +567,7 @@
|
||||
"chat_clearPath": "Effacer le chemin",
|
||||
"chat_clearPathSubtitle": "Forcer la redécouverte lors de la prochaine envoi",
|
||||
"chat_pathCleared": "Le chemin est dégagé. Le prochain message redécouvrira le tracé.",
|
||||
"chat_floodModeSubtitle": "Utiliser le commutateur de routage dans la barre d'application",
|
||||
"chat_floodModeSubtitle": "Désactive l'apprentissage du chemin (à éviter). Utiliser le commutateur de routage dans la barre d'application pour rebasculer en mode auto par la suite.",
|
||||
"chat_floodModeEnabled": "Le mode envoi à tout le réseau est activé. Changer via l'icône de routage dans la barre d'outils.",
|
||||
"chat_fullPath": "Chemin complet",
|
||||
"chat_pathDetailsNotAvailable": "Les détails du chemin ne sont pas encore disponibles. Essayez d'envoyer un message pour rafraîchir.",
|
||||
@@ -642,7 +643,7 @@
|
||||
},
|
||||
"map_chat": "Chat",
|
||||
"map_repeater": "Répéteur",
|
||||
"map_room": "Salle",
|
||||
"map_room": "Room Server",
|
||||
"map_sensor": "Capteur",
|
||||
"map_pinDm": "Clé (DM)",
|
||||
"map_pinPrivate": "Verrouiller (Privé)",
|
||||
@@ -681,7 +682,7 @@
|
||||
"map_showSharedMarkers": "Afficher les marqueurs partagés",
|
||||
"map_lastSeenTime": "Dernière fois vu",
|
||||
"map_sharedPin": "Clé partagée",
|
||||
"map_joinRoom": "Rejoindre la salle",
|
||||
"map_joinRoom": "Rejoindre le room server",
|
||||
"map_manageRepeater": "Gérer le répéteur",
|
||||
"mapCache_title": "Cache de Carte Hors Ligne",
|
||||
"mapCache_selectAreaFirst": "Sélectionner une zone pour la mise en cache en premier",
|
||||
@@ -864,7 +865,7 @@
|
||||
"path_labelHexPrefixes": "Préfixes hexadécimaux",
|
||||
"path_helperMaxHops": "Max 64 sauts. Chaque préfixe fait 2 caractères hexadécimaux (1 octet)",
|
||||
"path_selectFromContacts": "Sélectionner à partir des contacts :",
|
||||
"path_noRepeatersFound": "Aucun répéteur ou serveur de salle n'a été trouvé.",
|
||||
"path_noRepeatersFound": "Aucun répéteur ou room server n'a été trouvé.",
|
||||
"path_customPathsRequire": "Les chemins personnalisés nécessitent des sauts intermédiaires qui peuvent transmettre des messages.",
|
||||
"path_invalidHexPrefixes": "Préfixes hexadécimaux invalides : {prefixes}",
|
||||
"@path_invalidHexPrefixes": {
|
||||
@@ -995,15 +996,15 @@
|
||||
"repeater_txPower": "TX Puissance",
|
||||
"repeater_txPowerHelper": "1-30 dBm",
|
||||
"repeater_bandwidth": "Bande passante",
|
||||
"repeater_spreadingFactor": "Facteur de répartition",
|
||||
"repeater_codingRate": "Taux de codage",
|
||||
"repeater_spreadingFactor": "Facteur de répartition (SF)",
|
||||
"repeater_codingRate": "Taux de codage (CR)",
|
||||
"repeater_locationSettings": "Paramètres de localisation",
|
||||
"repeater_latitude": "Latitude",
|
||||
"repeater_latitudeHelper": "Degrés décimaux (par exemple, 37.7749)",
|
||||
"repeater_longitude": "Longitude",
|
||||
"repeater_longitudeHelper": "Degrés décimaux (par exemple, -122,4194)",
|
||||
"repeater_features": "Fonctionnalités",
|
||||
"repeater_packetForwarding": "Transfert de paquets",
|
||||
"repeater_packetForwarding": "Mode répéteur",
|
||||
"repeater_packetForwardingSubtitle": "Activer le répéteur pour transmettre des paquets",
|
||||
"repeater_guestAccess": "Accès Invité",
|
||||
"repeater_guestAccessSubtitle": "Autoriser l'accès invité en lecture seule",
|
||||
@@ -1377,7 +1378,7 @@
|
||||
"channels_joinPublicChannelDesc": "Tout le monde peut rejoindre ce canal.",
|
||||
"channels_joinHashtagChannel": "Rejoindre un Canal Hashtag",
|
||||
"channels_joinHashtagChannelDesc": "N'importe qui peut rejoindre les canaux #hashtag.",
|
||||
"channels_scanQrCode": "Scanner un code QR",
|
||||
"channels_scanQrCode": "Scanner un QR code",
|
||||
"channels_scanQrCodeComingSoon": "Bientôt disponible",
|
||||
"channels_enterHashtag": "Entrez le hashtag",
|
||||
"channels_hashtagHint": "ex. #equipe",
|
||||
@@ -1466,8 +1467,8 @@
|
||||
"community_join": "Rejoindre",
|
||||
"community_joinTitle": "Rejoindre la communauté",
|
||||
"community_joinConfirmation": "Souhaitez-vous rejoindre la communauté \"{name}\" ?",
|
||||
"community_scanQr": "Scanner la communauté QR",
|
||||
"community_scanInstructions": "Pointez l'appareil photo vers un code QR communautaire.",
|
||||
"community_scanQr": "Scanner un QR code de communauté",
|
||||
"community_scanInstructions": "Pointez l'appareil photo vers un QR code de communauté.",
|
||||
"community_showQr": "Afficher le QR Code",
|
||||
"community_publicChannel": "Communauté Publique",
|
||||
"community_hashtagChannel": "Hashtag Communauté",
|
||||
@@ -1478,13 +1479,13 @@
|
||||
"community_qrTitle": "Partager Communauté",
|
||||
"community_qrInstructions": "Scanner ce QR code pour rejoindre {name}",
|
||||
"community_hashtagPrivacyHint": "Les canaux hashtag de la communauté ne sont accessibles qu'aux membres de la communauté",
|
||||
"community_invalidQrCode": "Code QR de communauté non valide",
|
||||
"community_invalidQrCode": "QR code de communauté non valide",
|
||||
"community_alreadyMember": "Déjà membre",
|
||||
"community_alreadyMemberMessage": "Vous êtes déjà membre de \"{name}\".",
|
||||
"community_addPublicChannel": "Ajouter un Canal Public de la Communauté",
|
||||
"community_addPublicChannelHint": "Ajouter automatiquement le canal public pour cette communauté",
|
||||
"community_noCommunities": "Aucun groupe n'a été rejoint pour le moment.",
|
||||
"community_scanOrCreate": "Scanner un code QR ou créer une communauté pour commencer",
|
||||
"community_scanOrCreate": "Scanner un QR code ou créer une communauté pour commencer",
|
||||
"community_manageCommunities": "Gérer les Communautés",
|
||||
"community_delete": "Quitter la communauté",
|
||||
"community_deleteConfirm": "Quitter \"{name}\" ?",
|
||||
@@ -1534,10 +1535,10 @@
|
||||
}
|
||||
},
|
||||
"community_regenerateSecret": "Régénérer le secret",
|
||||
"community_regenerateSecretConfirm": "Régénérer la clé secrète pour \"{name}\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.",
|
||||
"community_regenerateSecretConfirm": "Régénérer la clé secrète pour \"{name}\" ? Tous les membres devront scanner le nouveau QR code pour continuer à communiquer.",
|
||||
"community_regenerate": "Régénérer",
|
||||
"community_secretRegenerated": "Mot de passe secret régénéré pour \"{name}\"",
|
||||
"community_scanToUpdateSecret": "Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"{name}\"",
|
||||
"community_scanToUpdateSecret": "Scanner le nouveau QR code pour mettre à jour le mot de passe pour \"{name}\"",
|
||||
"community_updateSecret": "Mettre à jour le secret",
|
||||
"community_secretUpdated": "Modification secrète mise à jour pour \"{name}\"",
|
||||
"@contacts_pathTraceTo": {
|
||||
@@ -1554,11 +1555,11 @@
|
||||
"contacts_pathTrace": "Traçage de chemin",
|
||||
"contacts_repeaterPathTrace": "Tracer le chemin vers le répéteur",
|
||||
"contacts_repeaterPing": "Pinguer le répéteur",
|
||||
"contacts_roomPathTrace": "Traçage du chemin vers le serveur de la salle",
|
||||
"contacts_roomPathTrace": "Traçage du chemin vers le room server",
|
||||
"contacts_chatTraceRoute": "Tracer le chemin",
|
||||
"contacts_pathTraceTo": "Tracer l'itinéraire vers {name}",
|
||||
"contacts_ping": "Ping",
|
||||
"contacts_roomPing": "Pinguer le serveur de la salle",
|
||||
"contacts_roomPing": "Pinguer le room server",
|
||||
"contacts_invalidAdvertFormat": "Données de contact non valides",
|
||||
"appSettings_languageUk": "Ukrainien",
|
||||
"appSettings_languageRu": "Russe",
|
||||
@@ -1583,12 +1584,12 @@
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{nouveau nœud} other{nouveaux nœuds}}",
|
||||
"notification_newTypeDiscovered": "Nouveau {contactType} découvert",
|
||||
"notification_receivedNewMessage": "Nouveau message reçu",
|
||||
"settings_gpxExportRepeaters": "Exporter les répéteurs / serveur de salle au format GPX",
|
||||
"settings_gpxExportRepeaters": "Exporter les répéteurs / room servers au format GPX",
|
||||
"settings_gpxExportRepeatersSubtitle": "Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.",
|
||||
"settings_gpxExportNoContacts": "Aucun contact à exporter.",
|
||||
"settings_gpxExportNotAvailable": "Non pris en charge sur votre appareil/Système d'exploitation",
|
||||
"settings_gpxExportError": "Une erreur s'est produite lors de l'exportation.",
|
||||
"settings_gpxExportRepeatersRoom": "Emplacements des serveurs de répéteur et de salle",
|
||||
"settings_gpxExportRepeatersRoom": "Emplacements des répéteurs et room servers",
|
||||
"settings_gpxExportContacts": "Exporter les compagnons au format GPX",
|
||||
"settings_gpxExportAll": "Exporter tous les contacts au format GPX",
|
||||
"settings_gpxExportAllSubtitle": "Exporte tous les contacts avec une localisation vers un fichier GPX.",
|
||||
@@ -1605,6 +1606,8 @@
|
||||
"map_removeLast": "Supprimer le dernier",
|
||||
"map_runTrace": "Exécuter la traçage de chemin",
|
||||
"scanner_bluetoothOffMessage": "Veuillez activer le Bluetooth pour rechercher des appareils.",
|
||||
"scanner_chromeRequired": "Navigateur Chrome requis",
|
||||
"scanner_chromeRequiredMessage": "Cette application web nécessite Google Chrome ou un navigateur basé sur Chromium pour le support Bluetooth.",
|
||||
"scanner_bluetoothOff": "Le Bluetooth est désactivé.",
|
||||
"scanner_enableBluetooth": "Activer le Bluetooth",
|
||||
"snrIndicator_lastSeen": "Dernière fois vu",
|
||||
@@ -1798,7 +1801,278 @@
|
||||
"contacts_unread": "Non lu",
|
||||
"contacts_searchFavorites": "Rechercher {number}{str} Favoris...",
|
||||
"contacts_searchUsers": "Rechercher {number}{str} utilisateurs...",
|
||||
"contacts_searchRoomServers": "Rechercher {number}{str} serveurs de salle...",
|
||||
"contacts_searchRoomServers": "Rechercher {number}{str} room server...",
|
||||
"contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...",
|
||||
"contacts_searchContactsNoNumber": "Rechercher des contacts..."
|
||||
"contacts_searchContactsNoNumber": "Rechercher des contacts...",
|
||||
"settings_contactSettings": "Paramètres de contact",
|
||||
"settings_contactSettingsSubtitle": "Paramètres pour l'ajout de contacts",
|
||||
"contactsSettings_autoAddRepeatersTitle": "Ajouter automatiquement les répéteurs",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "Autoriser le compagnon à ajouter automatiquement les répéteurs découverts",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Ajouter automatiquement les room servers",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Autoriser le compagnon à ajouter automatiquement les room servers découverts",
|
||||
"contactsSettings_otherTitle": "Autres paramètres liés aux contacts",
|
||||
"contactsSettings_title": "Paramètres des contacts",
|
||||
"contactsSettings_autoAddUsersTitle": "Ajouter automatiquement les utilisateurs",
|
||||
"contactsSettings_autoAddTitle": "Découverte automatique",
|
||||
"contactsSettings_autoAddSensorsTitle": "Ajouter automatiquement les capteurs",
|
||||
"contactsSettings_autoAddUsersSubtitle": "Autoriser le compagnon à ajouter automatiquement les utilisateurs découverts",
|
||||
"discoveredContacts_noMatching": "Aucun contact correspondant",
|
||||
"discoveredContacts_contactAdded": "Contact ajouté",
|
||||
"discoveredContacts_addContact": "Ajouter un contact",
|
||||
"discoveredContacts_copyContact": "Copier le contact dans le presse-papiers",
|
||||
"discoveredContacts_deleteContact": "Supprimer le contact",
|
||||
"contactsSettings_overwriteOldestTitle": "Écraser le plus ancien",
|
||||
"contactsSettings_autoAddSensorsSubtitle": "Autoriser le compagnon à ajouter automatiquement les capteurs découverts.",
|
||||
"discoveredContacts_Title": "Contacts découverts",
|
||||
"discoveredContacts_searchHint": "Rechercher des contacts découverts",
|
||||
"contactsSettings_overwriteOldestSubtitle": "Lorsque la liste de contacts est pleine, le contact le plus ancien non favori sera remplacé.",
|
||||
"common_deleteAll": "Supprimer tout",
|
||||
"discoveredContacts_deleteContactAll": "Supprimer tous les contacts découverts",
|
||||
"discoveredContacts_deleteContactAllContent": "Êtes-vous sûr de vouloir supprimer tous les contacts découverts ?",
|
||||
"map_showGuessedLocations": "Afficher les emplacements des nœuds estimés",
|
||||
"map_guessedLocation": "Lieu deviné",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"usbScreenTitle": "Connectez via USB",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"usbScreenSubtitle": "Sélectionnez un périphérique série détecté et connectez-vous directement à votre nœud MeshCore.",
|
||||
"usbScreenStatus": "Sélectionnez un périphérique USB",
|
||||
"usbScreenNote": "La communication série USB est active sur les appareils Android et les plateformes de bureau compatibles.",
|
||||
"usbScreenEmptyState": "Aucun périphérique USB n'a été trouvé. Veuillez en brancher un et rafraîchir la page.",
|
||||
"usbErrorPermissionDenied": "L'accès via USB a été refusé.",
|
||||
"usbErrorDeviceMissing": "Le périphérique USB sélectionné n'est plus disponible.",
|
||||
"usbErrorInvalidPort": "Sélectionnez un périphérique USB valide.",
|
||||
"usbErrorBusy": "Une autre demande de connexion USB est déjà en cours.",
|
||||
"usbErrorNotConnected": "Aucun appareil USB n'est connecté.",
|
||||
"usbErrorOpenFailed": "Impossible d'ouvrir l'appareil USB sélectionné.",
|
||||
"usbErrorConnectFailed": "Impossible de se connecter à l'appareil USB sélectionné.",
|
||||
"usbErrorUnsupported": "La communication série USB n'est pas prise en charge sur cette plateforme.",
|
||||
"usbErrorAlreadyActive": "Une connexion USB est déjà établie.",
|
||||
"usbErrorNoDeviceSelected": "Aucun appareil USB n'a été sélectionné.",
|
||||
"usbErrorPortClosed": "La connexion USB n'est pas établie.",
|
||||
"usbFallbackDeviceName": "Dispositif de communication série sur le Web",
|
||||
"@usbConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"usbStatus_notConnected": "Sélectionnez un périphérique USB",
|
||||
"usbConnectionFailed": "Échec de la connexion USB : {error}",
|
||||
"usbStatus_connecting": "Connexion au périphérique USB...",
|
||||
"usbStatus_searching": "Recherche de périphériques USB...",
|
||||
"usbErrorConnectTimedOut": "La connexion a expiré. Assurez-vous que l'appareil dispose du firmware USB Companion.",
|
||||
"@tcpStatus_connectingTo": {
|
||||
"placeholders": {
|
||||
"endpoint": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@tcpConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tcpHostLabel": "Adresse IP",
|
||||
"connectionChoiceTcpLabel": "TCP",
|
||||
"tcpScreenTitle": "Établir une connexion via TCP",
|
||||
"tcpHostHint": "192.168.40.10",
|
||||
"tcpPortLabel": "Port",
|
||||
"tcpPortHint": "5000",
|
||||
"tcpStatus_notConnected": "Entrez l'adresse de destination et connectez-vous.",
|
||||
"tcpStatus_connectingTo": "Connexion à {endpoint}...",
|
||||
"tcpErrorHostRequired": "Une adresse IP est obligatoire.",
|
||||
"tcpErrorPortInvalid": "La taille du port doit être comprise entre 1 et 65535.",
|
||||
"tcpErrorUnsupported": "Le protocole TCP n'est pas pris en charge sur cette plateforme.",
|
||||
"tcpErrorTimedOut": "La connexion TCP a expiré.",
|
||||
"tcpConnectionFailed": "Échec de la connexion TCP : {error}",
|
||||
"map_showDiscoveryContacts": "Afficher les contacts de découverte",
|
||||
"map_setAsMyLocation": "Définir comme ma localisation",
|
||||
"@path_routeWeight": {
|
||||
"placeholders": {
|
||||
"weight": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"appSettings_initialRouteWeightSubtitle": "Poids de départ pour les nouveaux chemins découverts",
|
||||
"appSettings_routeWeightSuccessIncrement": "Augmentation du poids de réussite",
|
||||
"appSettings_routeWeightSuccessIncrementSubtitle": "Poids ajouté à un itinéraire après une livraison réussie.",
|
||||
"appSettings_routeWeightFailureDecrement": "Réduction du poids de pénalité",
|
||||
"appSettings_routeWeightFailureDecrementSubtitle": "Poids retiré d'un itinéraire après une tentative de livraison infructueuse.",
|
||||
"appSettings_maxMessageRetries": "Nombre maximal de tentatives de récupération de messages",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Nombre de tentatives de relance avant de marquer un message comme ayant échoué.",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@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",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerTitle": "Traduire avant d'envoyer",
|
||||
"translation_enableTitle": "Activer la traduction",
|
||||
"translation_title": "Traduction",
|
||||
"translation_enableSubtitle": "Traduire les messages entrants et permettre la traduction avant l'envoi.",
|
||||
"translation_composerSubtitle": "Contrôle l'état par défaut de l'icône de traduction du composant.",
|
||||
"translation_targetLanguage": "Langue cible",
|
||||
"translation_useAppLanguage": "Utiliser la langue de l'application",
|
||||
"translation_downloadedModelLabel": "Modèle téléchargé",
|
||||
"translation_presetModelLabel": "Modèle Hugging Face préconfiguré",
|
||||
"translation_manualUrlLabel": "URL du modèle manuel",
|
||||
"translation_downloadModel": "Télécharger le modèle",
|
||||
"translation_downloading": "Téléchargement...",
|
||||
"translation_working": "Au travail...",
|
||||
"translation_stop": "Arrêtez",
|
||||
"translation_mergingChunks": "Fusion des fragments téléchargés dans le fichier final...",
|
||||
"translation_downloadedModels": "Modèles téléchargés",
|
||||
"translation_deleteModel": "Supprimer le modèle",
|
||||
"translation_modelDownloaded": "Modèle de traduction téléchargé.",
|
||||
"translation_downloadStopped": "Le téléchargement a été interrompu.",
|
||||
"translation_downloadFailed": "Échec du téléchargement : {error}",
|
||||
"translation_enterUrlFirst": "Entrez d'abord l'URL du modèle.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerEnabledHint": "Les messages seront traduits avant d'être envoyés.",
|
||||
"translation_translateBeforeSending": "Traduire avant d'envoyer",
|
||||
"translation_composerDisabledHint": "Envoyez des messages dans la langue originale, telle que vous l'avez tapée.",
|
||||
"translation_messageTranslation": "Traduction du message",
|
||||
"translation_translateTo": "Traduire en {language}",
|
||||
"translation_translationOptions": "Options de traduction",
|
||||
"translation_systemLanguage": "Langue du système",
|
||||
"scanner_linuxPairingPinTitle": "Code PIN pour la connexion Bluetooth",
|
||||
"scanner_linuxPairingHidePin": "Masquer le code PIN",
|
||||
"scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si nécessaire).",
|
||||
"scanner_linuxPairingShowPin": "Afficher le code PIN",
|
||||
"repeater_cliQuickClockSync": "Synchronisation de l'horloge",
|
||||
"repeater_cliQuickDiscovery": "Découvrir les voisins",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Envoyer automatiquement une notification \"synchronisation de l'heure\" après une connexion réussie.",
|
||||
"repeater_clockSyncAfterLogin": "Synchronisation de l'horloge après la connexion",
|
||||
"repeater_guestTools": "Outils pour les invités",
|
||||
"chat_sendMessage": "Envoyer un message",
|
||||
"room_guest": "Informations sur le serveur",
|
||||
"repeater_guest": "Informations sur les répéteurs"
|
||||
}
|
||||
|
||||
+2115
File diff suppressed because it is too large
Load Diff
+275
-1
@@ -285,6 +285,7 @@
|
||||
"contacts_newGroup": "Nuovo Gruppo",
|
||||
"contacts_groupName": "Nome gruppo",
|
||||
"contacts_groupNameRequired": "Il nome del gruppo è obbligatorio.",
|
||||
"contacts_groupNameReserved": "Questo nome del gruppo è riservato",
|
||||
"contacts_groupAlreadyExists": "Il gruppo \"{name}\" esiste già.",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -1606,6 +1607,8 @@
|
||||
"map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.",
|
||||
"scanner_bluetoothOff": "Il Bluetooth è disattivato.",
|
||||
"scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.",
|
||||
"scanner_chromeRequired": "Browser Chrome richiesto",
|
||||
"scanner_chromeRequiredMessage": "Questa applicazione web richiede Google Chrome o un browser basato su Chromium per il supporto Bluetooth.",
|
||||
"scanner_enableBluetooth": "Abilita il Bluetooth",
|
||||
"snrIndicator_nearByRepeaters": "Ripetitori vicini",
|
||||
"snrIndicator_lastSeen": "Ultimo accesso",
|
||||
@@ -1800,5 +1803,276 @@
|
||||
"contacts_searchFavorites": "Cerca {number}{str} Preferiti...",
|
||||
"contacts_unread": "Non letti",
|
||||
"contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...",
|
||||
"contacts_searchRoomServers": "Cerca {number}{str} server Room..."
|
||||
"contacts_searchRoomServers": "Cerca {number}{str} server Room...",
|
||||
"contactsSettings_title": "Impostazioni dei contatti",
|
||||
"settings_contactSettings": "Impostazioni di contatto",
|
||||
"contactsSettings_otherTitle": "Altre impostazioni relative ai contatti",
|
||||
"contactsSettings_autoAddUsersSubtitle": "Consenti al compagno di aggiungere automaticamente gli utenti scoperti.",
|
||||
"contactsSettings_autoAddRepeatersTitle": "Aggiungere ripetitori automaticamente",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Consenti al compagno di aggiungere automaticamente i server delle stanze scoperte.",
|
||||
"contactsSettings_autoAddSensorsTitle": "Aggiungere automaticamente i sensori",
|
||||
"settings_contactSettingsSubtitle": "Impostazioni per l'aggiunta dei contatti",
|
||||
"contactsSettings_autoAddUsersTitle": "Aggiungere utenti automaticamente",
|
||||
"contactsSettings_autoAddTitle": "Scoperta automatica",
|
||||
"contactsSettings_autoAddSensorsSubtitle": "Consenti al compagno di aggiungere automaticamente i sensori scoperti",
|
||||
"discoveredContacts_noMatching": "Nessun contatto corrispondente",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "Consenti al compagno di aggiungere automaticamente i ripetitori scoperti.",
|
||||
"discoveredContacts_searchHint": "Cerca contatti scoperti",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Aggiungere automaticamente i server delle stanze",
|
||||
"discoveredContacts_addContact": "Aggiungi contatto",
|
||||
"contactsSettings_overwriteOldestTitle": "Sostituisci il più vecchio",
|
||||
"discoveredContacts_Title": "Contatti scoperti",
|
||||
"discoveredContacts_contactAdded": "Contatto aggiunto",
|
||||
"discoveredContacts_deleteContact": "Elimina Contatto",
|
||||
"discoveredContacts_copyContact": "Copia contatto negli appunti",
|
||||
"contactsSettings_overwriteOldestSubtitle": "Quando l'elenco dei contatti è pieno, il contatto più vecchio non tra i preferiti verrà sostituito.",
|
||||
"common_deleteAll": "Elimina tutto",
|
||||
"discoveredContacts_deleteContactAllContent": "Sei sicuro di voler eliminare tutti i contatti scoperti?",
|
||||
"discoveredContacts_deleteContactAll": "Eliminare tutti i contatti scoperti",
|
||||
"map_guessedLocation": "Località indovinata",
|
||||
"map_showGuessedLocations": "Mostra le posizioni stimate dei nodi",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"usbScreenSubtitle": "Seleziona il dispositivo seriale rilevato e connettilo direttamente al tuo nodo MeshCore.",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"usbScreenTitle": "Connessione tramite USB",
|
||||
"usbScreenStatus": "Seleziona un dispositivo USB",
|
||||
"usbScreenNote": "La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.",
|
||||
"usbScreenEmptyState": "Nessun dispositivo USB rilevato. Collegare uno e aggiornare.",
|
||||
"usbErrorPermissionDenied": "È stato negato l'accesso tramite USB.",
|
||||
"usbErrorDeviceMissing": "Il dispositivo USB selezionato non è più disponibile.",
|
||||
"usbErrorInvalidPort": "Seleziona un dispositivo USB valido.",
|
||||
"usbErrorBusy": "Un'altra richiesta di connessione tramite USB è già in corso.",
|
||||
"usbErrorNotConnected": "Non è collegato alcun dispositivo USB.",
|
||||
"usbErrorOpenFailed": "Impossibile aprire il dispositivo USB selezionato.",
|
||||
"usbErrorConnectFailed": "Impossibile connettersi al dispositivo USB selezionato.",
|
||||
"usbErrorUnsupported": "La comunicazione seriale tramite USB non è supportata su questa piattaforma.",
|
||||
"usbErrorAlreadyActive": "La connessione USB è già attiva.",
|
||||
"usbErrorNoDeviceSelected": "Non è stato selezionato alcun dispositivo USB.",
|
||||
"usbErrorPortClosed": "La connessione USB non è attiva.",
|
||||
"usbFallbackDeviceName": "Dispositivo per comunicazione seriale su rete",
|
||||
"@usbConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"usbStatus_searching": "Ricerca di dispositivi USB...",
|
||||
"usbConnectionFailed": "Errore nella connessione USB: {error}",
|
||||
"usbStatus_notConnected": "Seleziona un dispositivo USB",
|
||||
"usbStatus_connecting": "Connessione al dispositivo USB...",
|
||||
"usbErrorConnectTimedOut": "La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion.",
|
||||
"@tcpStatus_connectingTo": {
|
||||
"placeholders": {
|
||||
"endpoint": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@tcpConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tcpHostLabel": "Indirizzo IP",
|
||||
"tcpHostHint": "192.168.40.10",
|
||||
"connectionChoiceTcpLabel": "TCP",
|
||||
"tcpScreenTitle": "Stabilire una connessione tramite TCP",
|
||||
"tcpPortLabel": "Porta",
|
||||
"tcpPortHint": "5000",
|
||||
"tcpStatus_notConnected": "Inserisci l'endpoint e connettiti.",
|
||||
"tcpStatus_connectingTo": "Connessione a {endpoint}...",
|
||||
"tcpErrorHostRequired": "È necessario fornire un indirizzo IP.",
|
||||
"tcpErrorPortInvalid": "La dimensione della porta deve essere compresa tra 1 e 65535.",
|
||||
"tcpErrorUnsupported": "Il protocollo TCP non è supportato su questa piattaforma.",
|
||||
"tcpErrorTimedOut": "La connessione TCP è scaduta.",
|
||||
"tcpConnectionFailed": "Impossibile stabilire la connessione TCP: {error}",
|
||||
"map_showDiscoveryContacts": "Mostra Contatti di Discovery",
|
||||
"map_setAsMyLocation": "Imposta come la mia posizione",
|
||||
"@path_routeWeight": {
|
||||
"placeholders": {
|
||||
"weight": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
"appSettings_maxRouteWeight": "Massimo peso consentito per il percorso",
|
||||
"appSettings_routeWeightSuccessIncrement": "Aumento del peso del successo",
|
||||
"appSettings_routeWeightSuccessIncrementSubtitle": "Peso aggiunto a un percorso dopo una consegna riuscita.",
|
||||
"appSettings_routeWeightFailureDecrement": "Riduzione del peso associato al fallimento",
|
||||
"appSettings_routeWeightFailureDecrementSubtitle": "Peso rimosso da un percorso dopo un tentativo di consegna fallito.",
|
||||
"appSettings_maxMessageRetries": "Numero massimo di tentativi di invio del messaggio",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Numero di tentativi di riprova prima di considerare un messaggio come fallito.",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@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",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerTitle": "Tradurre prima di inviare",
|
||||
"translation_enableSubtitle": "Tradurre i messaggi in arrivo e consentire la traduzione preventiva prima dell'invio.",
|
||||
"translation_enableTitle": "Abilitare la traduzione",
|
||||
"translation_title": "Traduzione",
|
||||
"translation_composerSubtitle": "Controlla lo stato predefinito dell'icona di traduzione del compositore.",
|
||||
"translation_targetLanguage": "Lingua di destinazione",
|
||||
"translation_useAppLanguage": "Utilizza la lingua dell'app",
|
||||
"translation_downloadedModelLabel": "Modello scaricato",
|
||||
"translation_presetModelLabel": "Modello predefinito di Hugging Face",
|
||||
"translation_manualUrlLabel": "URL del modello manuale",
|
||||
"translation_downloadModel": "Scarica il modello",
|
||||
"translation_downloading": "Inizio download...",
|
||||
"translation_working": "Lavoro...",
|
||||
"translation_stop": "Smetta",
|
||||
"translation_downloadedModels": "Modelli scaricati",
|
||||
"translation_mergingChunks": "Unione dei frammenti scaricati in un unico file...",
|
||||
"translation_deleteModel": "Elimina modello",
|
||||
"translation_modelDownloaded": "Modello di traduzione scaricato.",
|
||||
"translation_downloadStopped": "Il download è stato interrotto.",
|
||||
"translation_downloadFailed": "Download fallito: {error}",
|
||||
"translation_enterUrlFirst": "Inserite innanzitutto l'URL del modello.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_messageTranslation": "Traduzione del messaggio",
|
||||
"translation_translateBeforeSending": "Tradurre prima di inviare",
|
||||
"translation_composerDisabledHint": "Invia messaggi utilizzando la lingua originale, scritta.",
|
||||
"translation_composerEnabledHint": "I messaggi verranno tradotti prima di essere inviati.",
|
||||
"translation_translateTo": "Tradurre in {language}",
|
||||
"translation_translationOptions": "Opzioni di traduzione",
|
||||
"translation_systemLanguage": "Lingua del sistema",
|
||||
"scanner_linuxPairingPinPrompt": "Inserire il codice PIN per {deviceName} (lasciare vuoto se non presente).",
|
||||
"scanner_linuxPairingShowPin": "Mostra PIN",
|
||||
"scanner_linuxPairingPinTitle": "PIN per l'accoppiamento Bluetooth",
|
||||
"scanner_linuxPairingHidePin": "Nascondi il PIN",
|
||||
"repeater_cliQuickClockSync": "Sincronizzazione dell'orologio",
|
||||
"repeater_cliQuickDiscovery": "Scopri i Vicini",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Invia automaticamente il comando \"sincronizzazione dell'orologio\" dopo un login riuscito.",
|
||||
"repeater_clockSyncAfterLogin": "Sincronizzazione dell'orologio dopo il login",
|
||||
"repeater_guest": "Informazioni sul ripetitore",
|
||||
"repeater_guestTools": "Strumenti per gli ospiti",
|
||||
"chat_sendMessage": "Invia messaggio",
|
||||
"room_guest": "Informazioni sul server"
|
||||
}
|
||||
|
||||
+2115
File diff suppressed because it is too large
Load Diff
+2115
File diff suppressed because it is too large
Load Diff
+1032
-15
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get common_delete => 'Изтрий';
|
||||
|
||||
@override
|
||||
String get common_deleteAll => 'Изтрий всичко';
|
||||
|
||||
@override
|
||||
String get common_close => 'Затвори';
|
||||
|
||||
@@ -108,6 +111,134 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTcpLabel => 'TCP';
|
||||
|
||||
@override
|
||||
String get tcpScreenTitle => 'Свържете се чрез TCP';
|
||||
|
||||
@override
|
||||
String get tcpHostLabel => 'IP адрес';
|
||||
|
||||
@override
|
||||
String get tcpHostHint => '192.168.40.10';
|
||||
|
||||
@override
|
||||
String get tcpPortLabel => 'Пристанище';
|
||||
|
||||
@override
|
||||
String get tcpPortHint => '5000';
|
||||
|
||||
@override
|
||||
String get tcpStatus_notConnected => 'Въведете крайната точка и свържете се.';
|
||||
|
||||
@override
|
||||
String tcpStatus_connectingTo(String endpoint) {
|
||||
return 'Свързване към $endpoint...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tcpErrorHostRequired => 'Необходим е IP адрес.';
|
||||
|
||||
@override
|
||||
String get tcpErrorPortInvalid => 'Портът трябва да бъде между 1 и 65535.';
|
||||
|
||||
@override
|
||||
String get tcpErrorUnsupported =>
|
||||
'Транспортът чрез TCP не се поддържа на тази платформа.';
|
||||
|
||||
@override
|
||||
String get tcpErrorTimedOut => 'Връзката TCP изтекла.';
|
||||
|
||||
@override
|
||||
String tcpConnectionFailed(String error) {
|
||||
return 'Неуспешно е установено TCP връзката: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Свържете се чрез USB';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Изберете открития сериен уред и свържете директно към вашия MeshCore възел.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Изберете USB устройство';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Няма открити USB устройства. Включете едно и опитайте отново.';
|
||||
|
||||
@override
|
||||
String get usbErrorPermissionDenied => 'Не беше разрешено достъпът през USB.';
|
||||
|
||||
@override
|
||||
String get usbErrorDeviceMissing =>
|
||||
'Избраното USB устройство вече не е налично.';
|
||||
|
||||
@override
|
||||
String get usbErrorInvalidPort => 'Изберете валитно USB устройство.';
|
||||
|
||||
@override
|
||||
String get usbErrorBusy =>
|
||||
'Друг мол за свързване през USB вече е в процес на изпълнение.';
|
||||
|
||||
@override
|
||||
String get usbErrorNotConnected => 'Няма свързано USB устройство.';
|
||||
|
||||
@override
|
||||
String get usbErrorOpenFailed =>
|
||||
'Не успях да отворя избраното USB устройство.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectFailed =>
|
||||
'Не успях да се свържа с избраното USB устройство.';
|
||||
|
||||
@override
|
||||
String get usbErrorUnsupported =>
|
||||
'USB серийната комуникация не се поддържа на тази платформа.';
|
||||
|
||||
@override
|
||||
String get usbErrorAlreadyActive => 'USB връзката вече е активирана.';
|
||||
|
||||
@override
|
||||
String get usbErrorNoDeviceSelected => 'Няма избран USB устройство.';
|
||||
|
||||
@override
|
||||
String get usbErrorPortClosed => 'USB връзката не е активна.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectTimedOut =>
|
||||
'Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка.';
|
||||
|
||||
@override
|
||||
String get usbFallbackDeviceName =>
|
||||
'Устройство за четене на уеб серийни данни';
|
||||
|
||||
@override
|
||||
String get usbStatus_notConnected => 'Изберете USB устройство';
|
||||
|
||||
@override
|
||||
String get usbStatus_connecting => 'Свързване към USB устройство...';
|
||||
|
||||
@override
|
||||
String get usbStatus_searching => 'Търсене на USB устройства...';
|
||||
|
||||
@override
|
||||
String usbConnectionFailed(String error) {
|
||||
return 'Неуспешно свързване през USB: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Сканиране за устройства...';
|
||||
|
||||
@@ -150,6 +281,13 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Моля, активирайте Bluetooth, за да сканирате за устройства.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Изисква се браузър Chrome';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Активирайте Bluetooth';
|
||||
|
||||
@@ -234,6 +372,13 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get settings_longitude => 'Дължина';
|
||||
|
||||
@override
|
||||
String get settings_contactSettings => 'Настройки за контакти';
|
||||
|
||||
@override
|
||||
String get settings_contactSettingsSubtitle =>
|
||||
'Настройки за добавяне на контакти.';
|
||||
|
||||
@override
|
||||
String get settings_privacyMode => 'Режим на поверителност';
|
||||
|
||||
@@ -253,6 +398,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 => 'Действия';
|
||||
|
||||
@@ -550,6 +741,51 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
String get appSettings_autoRouteRotationDisabled =>
|
||||
'Автоматично маршрутизирането е деактивирано';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeight =>
|
||||
'Максимално допустимо тегло на маршрута';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeightSubtitle =>
|
||||
'Максималното тегло, което един маршрут може да събере от успешни доставки.';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeight =>
|
||||
'Първоначална тежест на маршрута';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeightSubtitle =>
|
||||
'Начално тегло за новооткрити маршрути';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrement =>
|
||||
'Увеличение на теглото за успех';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||
'Тегло, добавено към път след успешно доставяне.';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrement =>
|
||||
'Намаляване на теглото, свързано с неуспех';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||
'Тегло, което е било премахнато от пътя след неуспешен опит за доставка.';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetries =>
|
||||
'Максимален брой опити за изпращане на съобщение';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Батерия';
|
||||
|
||||
@@ -757,6 +993,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'Името на групата е задължително.';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => 'Това име на група е запазено';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'Групата \"$name\" вече съществува.';
|
||||
@@ -796,6 +1035,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 => 'Канали';
|
||||
|
||||
@@ -964,6 +1239,14 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'Няма съобщения.';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Изпратете съобщение';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Изпрати съобщение на $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => 'Изпрати съобщение, за да започнеш.';
|
||||
|
||||
@@ -983,11 +1266,6 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Местоположение';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Изпрати съобщение на $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Въведете съобщение...';
|
||||
|
||||
@@ -1366,6 +1644,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Споделете маркер тук';
|
||||
|
||||
@override
|
||||
String get map_setAsMyLocation => 'Задайте като моя местоположение';
|
||||
|
||||
@override
|
||||
String get map_pinLabel => 'Етикетиране на пин';
|
||||
|
||||
@@ -1411,6 +1692,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get map_otherNodes => 'Други възли';
|
||||
|
||||
@override
|
||||
String get map_showOverlaps => 'Покриване на ключа на повтаряча';
|
||||
|
||||
@override
|
||||
String get map_keyPrefix => 'Префикс на ключа';
|
||||
|
||||
@@ -1426,6 +1710,16 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get map_showSharedMarkers => 'Покажи споделени маркери';
|
||||
|
||||
@override
|
||||
String get map_showGuessedLocations =>
|
||||
'Покажете местоположенията на предположените възли.';
|
||||
|
||||
@override
|
||||
String get map_showDiscoveryContacts => 'Покажи контакти за откриване';
|
||||
|
||||
@override
|
||||
String get map_guessedLocation => 'Предполагано местоположение';
|
||||
|
||||
@override
|
||||
String get map_lastSeenTime => 'Последна видяна дата';
|
||||
|
||||
@@ -1445,6 +1739,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get map_runTrace => 'Изпълни Път на Следване';
|
||||
|
||||
@override
|
||||
String get map_runTraceWithReturnPath => 'Върни се по същия път.';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Премахни Последно';
|
||||
|
||||
@@ -1722,9 +2019,18 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'Управление на сървъра за стая';
|
||||
|
||||
@override
|
||||
String get repeater_guest => 'Информация за ретранслаторите';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Информация за сървъра на стаята';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Инструменти за управление';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Инструменти за гости';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Статус';
|
||||
|
||||
@@ -1759,6 +2065,14 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
String get repeater_settingsSubtitle =>
|
||||
'Конфигурирайте параметрите на репитера';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin =>
|
||||
'Синхронизиране на часовника след влизане';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Автоматично изпращайте съобщение \"синхронизиране на часовника\" след успешно влизане.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'Статус на повтарянето';
|
||||
|
||||
@@ -2135,6 +2449,12 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Часовник';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'Синхронизация на часовника';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Открий Съседи';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Изпраща рекламен пакет';
|
||||
|
||||
@@ -3115,4 +3435,274 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Последно видян';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Настройки на контактите';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Автоматично откриване';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Други настройки свързани с контакти';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Автоматично добавяне на потребители';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Позволи на спътника да добавя автоматично откритите потребители.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Автоматично добавяне на повтарящи се елементи';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Позволи на спътника да добавя автоматично откритите повтарящи се устройства.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Автоматично добавяне на сървъри на стаите';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Позволи на спътника да добавя автоматично откритите сървъри на стаите.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Автоматично добавяне на датчици';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Позволи на спътника да добавя автоматично откритите датчици.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle => 'Премахни най-старото';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'Когато списъкът с контакти е пълен, най-старият неключов контакт ще бъде заменен.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Открити контакти';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => 'Няма съвпадащи контакти';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Търсене на открити контакти';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Контакт добавен';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Добави контакт';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact => 'Копирай контакт в клипборда';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Изтрий контакт';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Изтриване на Всички Открити Контакти';
|
||||
|
||||
@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 и време на пренос';
|
||||
|
||||
@override
|
||||
String get translation_title => 'Превод';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Активирайте превода';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Превеждайте входящите съобщения и позволявайте предварително превеждане преди изпращане.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Преведете преди да изпратите';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Контролира началния статус на иконата за превод, създадена от композитора.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Целеви език';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => 'Използвайте езика на приложението';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Изтегнат модел';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel =>
|
||||
'Предварително конфигуриран модел от Hugging Face';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => 'URL на ръководството';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Изтеглете модела';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Изтегляне...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Работа...';
|
||||
|
||||
@override
|
||||
String get translation_stop => 'Спрете';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Съединяване на изтеглените части в един файл...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Изтеглени модели';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Изтриване на модела';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded => 'Моделът за превод е изтеглен.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'Изтеглянето беше прекъснато.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'Не успях да изтегля: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst => 'Въведете първо URL адрес на модела.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Покажи PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingHidePin => 'Скриване на PIN кода';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingPinTitle => 'PIN за съвпадение чрез Bluetooth';
|
||||
|
||||
@override
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Въведете PIN кода за $deviceName (оставете празно, ако няма такъв).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Превод на съобщението';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending =>
|
||||
'Преведете преди да изпратите';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'Съобщенията ще бъдат преведени, преди да бъдат изпратени.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Изпращайте съобщения на оригиналния въведен език.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Превеждане на $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Опции за превод';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Език на системата';
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get common_delete => 'Löschen';
|
||||
|
||||
@override
|
||||
String get common_deleteAll => 'Alles löschen';
|
||||
|
||||
@override
|
||||
String get common_close => 'Schließen';
|
||||
|
||||
@@ -108,6 +111,137 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTcpLabel => 'TCP';
|
||||
|
||||
@override
|
||||
String get tcpScreenTitle => 'Verbinden über TCP';
|
||||
|
||||
@override
|
||||
String get tcpHostLabel => 'IP-Adresse';
|
||||
|
||||
@override
|
||||
String get tcpHostHint => '192.168.40.10';
|
||||
|
||||
@override
|
||||
String get tcpPortLabel => 'Port';
|
||||
|
||||
@override
|
||||
String get tcpPortHint => '5000';
|
||||
|
||||
@override
|
||||
String get tcpStatus_notConnected =>
|
||||
'Geben Sie den Endpunkt ein und verbinden Sie sich.';
|
||||
|
||||
@override
|
||||
String tcpStatus_connectingTo(String endpoint) {
|
||||
return 'Verbindung zu $endpoint...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tcpErrorHostRequired => 'Eine IP-Adresse ist erforderlich.';
|
||||
|
||||
@override
|
||||
String get tcpErrorPortInvalid =>
|
||||
'Die Portnummer muss zwischen 1 und 65535 liegen.';
|
||||
|
||||
@override
|
||||
String get tcpErrorUnsupported =>
|
||||
'Die TCP-Übertragung wird auf dieser Plattform nicht unterstützt.';
|
||||
|
||||
@override
|
||||
String get tcpErrorTimedOut => 'Die TCP-Verbindung ist abgelaufen.';
|
||||
|
||||
@override
|
||||
String tcpConnectionFailed(String error) {
|
||||
return 'Fehler beim TCP-Verbindungsaufbau: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Verbinden über USB';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Wählen Sie ein USB-Gerät aus';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'Die USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie.';
|
||||
|
||||
@override
|
||||
String get usbErrorPermissionDenied =>
|
||||
'Die USB-Berechtigung wurde abgelehnt.';
|
||||
|
||||
@override
|
||||
String get usbErrorDeviceMissing =>
|
||||
'Das ausgewählte USB-Gerät ist nicht mehr verfügbar.';
|
||||
|
||||
@override
|
||||
String get usbErrorInvalidPort => 'Wählen Sie ein gültiges USB-Gerät aus.';
|
||||
|
||||
@override
|
||||
String get usbErrorBusy =>
|
||||
'Eine weitere Anfrage für eine USB-Verbindung ist bereits in Bearbeitung.';
|
||||
|
||||
@override
|
||||
String get usbErrorNotConnected => 'Es ist kein USB-Gerät angeschlossen.';
|
||||
|
||||
@override
|
||||
String get usbErrorOpenFailed =>
|
||||
'Fehlgeschlagen beim Öffnen des ausgewählten USB-Geräts.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectFailed =>
|
||||
'Keine Verbindung zum ausgewählten USB-Gerät hergestellt.';
|
||||
|
||||
@override
|
||||
String get usbErrorUnsupported =>
|
||||
'Die USB-Serielle Schnittstelle wird auf dieser Plattform nicht unterstützt.';
|
||||
|
||||
@override
|
||||
String get usbErrorAlreadyActive =>
|
||||
'Eine USB-Verbindung ist bereits hergestellt.';
|
||||
|
||||
@override
|
||||
String get usbErrorNoDeviceSelected => 'Kein USB-Gerät wurde ausgewählt.';
|
||||
|
||||
@override
|
||||
String get usbErrorPortClosed => 'Die USB-Verbindung ist nicht aktiv.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectTimedOut =>
|
||||
'Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält.';
|
||||
|
||||
@override
|
||||
String get usbFallbackDeviceName => 'Web-Serielle Geräte';
|
||||
|
||||
@override
|
||||
String get usbStatus_notConnected => 'Wählen Sie ein USB-Gerät aus';
|
||||
|
||||
@override
|
||||
String get usbStatus_connecting => 'Verbindung zum USB-Gerät...';
|
||||
|
||||
@override
|
||||
String get usbStatus_searching => 'Suche nach USB-Geräten...';
|
||||
|
||||
@override
|
||||
String usbConnectionFailed(String error) {
|
||||
return 'Fehler beim USB-Verbindungsaufbau: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Scannen nach Geräten...';
|
||||
|
||||
@@ -150,6 +284,13 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Chrome Browser erforderlich';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Diese Webanwendung erfordert Google Chrome oder einen Chromium-basierten Browser für die Bluetooth-Unterstützung.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Bluetooth aktivieren';
|
||||
|
||||
@@ -233,6 +374,13 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get settings_longitude => 'Längengrad';
|
||||
|
||||
@override
|
||||
String get settings_contactSettings => 'Kontakteinstellungen';
|
||||
|
||||
@override
|
||||
String get settings_contactSettingsSubtitle =>
|
||||
'Einstellungen für das Hinzufügen von Kontakten';
|
||||
|
||||
@override
|
||||
String get settings_privacyMode => 'Privatsphäreeinstellung';
|
||||
|
||||
@@ -250,6 +398,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';
|
||||
|
||||
@@ -547,6 +739,49 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get appSettings_autoRouteRotationDisabled =>
|
||||
'Automatische Routenrotation deaktiviert';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeight => 'Maximale Gesamtstreckenlänge';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeightSubtitle =>
|
||||
'Maximales Gewicht, das ein Weg durch erfolgreiche Lieferungen erreichen kann.';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeight => 'Anfangs-Streckengewicht';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeightSubtitle =>
|
||||
'Ausgangsgewicht für neu entdeckte Pfade';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrement =>
|
||||
'Erhöhung des Erfolgsgewichts';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||
'Gewicht, das einem Pfad nach erfolgreicher Lieferung hinzugefügt wird.';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrement =>
|
||||
'Reduzierung des Gewichts bei Fehlern';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||
'Gewicht, das nach einem fehlgeschlagenen Versand von einem Weg entfernt wurde';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetries =>
|
||||
'Maximale Anzahl an Wiederholungsversuchen';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Akku';
|
||||
|
||||
@@ -754,6 +989,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'Der Gruppennamen ist erforderlich.';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => 'Dieser Gruppenname ist reserviert';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'Die Gruppe \"$name\" existiert bereits.';
|
||||
@@ -770,29 +1008,64 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get contacts_noMembers => 'Keine Mitglieder';
|
||||
|
||||
@override
|
||||
String get contacts_lastSeenNow => 'gerade gesehen';
|
||||
String get contacts_lastSeenNow => 'kürzlich';
|
||||
|
||||
@override
|
||||
String contacts_lastSeenMinsAgo(int minutes) {
|
||||
return 'Letzte Sichtung vor $minutes Minuten.';
|
||||
return '~ $minutes Min.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_lastSeenHourAgo => 'Letzte Sichtung vor 1 Stunde.';
|
||||
String get contacts_lastSeenHourAgo => '~ 1 Std.';
|
||||
|
||||
@override
|
||||
String contacts_lastSeenHoursAgo(int hours) {
|
||||
return 'Letzte Sichtung vor $hours Stunden.';
|
||||
return '~ $hours Std.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_lastSeenDayAgo => 'Letzte Sichtung vor 1 Tag';
|
||||
String get contacts_lastSeenDayAgo => '~ 1 Tag';
|
||||
|
||||
@override
|
||||
String contacts_lastSeenDaysAgo(int days) {
|
||||
return 'Letzte Sichtung $days Tage zuvor';
|
||||
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';
|
||||
|
||||
@@ -965,6 +1238,14 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'Noch keine Nachrichten.';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Nachricht senden';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Sende eine Nachricht an $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => 'Eine Nachricht senden, um anzufangen.';
|
||||
|
||||
@@ -984,11 +1265,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Ort';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Sende eine Nachricht an $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Eine Nachricht eingeben...';
|
||||
|
||||
@@ -1365,6 +1641,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Teilen Sie den Marker hier.';
|
||||
|
||||
@override
|
||||
String get map_setAsMyLocation => 'Als meine aktuelle Position festlegen';
|
||||
|
||||
@override
|
||||
String get map_pinLabel => 'Pin Name';
|
||||
|
||||
@@ -1410,6 +1689,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';
|
||||
|
||||
@@ -1425,6 +1707,16 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get map_showSharedMarkers => 'Zeige gemeinsam genutzte Marker';
|
||||
|
||||
@override
|
||||
String get map_showGuessedLocations =>
|
||||
'Zeige die vermuteten Knotenpositionen';
|
||||
|
||||
@override
|
||||
String get map_showDiscoveryContacts => 'Entdeckungs-Kontakte anzeigen';
|
||||
|
||||
@override
|
||||
String get map_guessedLocation => 'Geschätzter Ort';
|
||||
|
||||
@override
|
||||
String get map_lastSeenTime => 'Letzte Sichtung';
|
||||
|
||||
@@ -1444,6 +1736,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';
|
||||
|
||||
@@ -1721,9 +2017,18 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'Raum-Server-Verwaltung';
|
||||
|
||||
@override
|
||||
String get repeater_guest => 'Informationen zu Repeatern';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Informationen zum Room Server';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Verwaltungs-Tools';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Gastwerkzeuge';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Status';
|
||||
|
||||
@@ -1756,6 +2061,14 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_settingsSubtitle => 'Repeater-parameter konfigurieren';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin =>
|
||||
'Uhrzeit-Synchronisation nach dem Anmelden';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Automatisch \"Uhrzeit-Synchronisierung\" nach erfolgreicher Anmeldung senden.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'Repeaterstatus';
|
||||
|
||||
@@ -2136,6 +2449,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Uhr';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'Uhr Synchronisieren';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Entdecke Nachbarn';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Sendet eine Ankündigung';
|
||||
|
||||
@@ -3124,4 +3443,277 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Zuletzt gesehen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Kontakteinstellungen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Automatische Erkennung';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Weitere Einstellungen zu Kontakten';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Automatische Hinzufügung von Benutzern';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Ermöglichen Sie dem Begleiter, automatisch entdeckte Benutzer hinzuzufügen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Automatisch Repeater hinzufügen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Ermöglichen Sie dem Begleiter, automatisch entdeckte Repeater hinzuzufügen.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Automatisch Raumservers hinzufügen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Ermöglichen Sie dem Begleiter, entdeckte Raumserver automatisch hinzuzufügen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Automatisch Sensoren hinzufügen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Ermöglichen Sie dem Begleiter, automatisch entdeckte Sensoren hinzuzufügen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle =>
|
||||
'Überschreiben des Ältesten';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'Wenn die Kontaktliste voll ist, wird der älteste nicht favorisierte Kontakt ersetzt.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Entdeckte Kontakte';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => 'Keine passenden Kontakte';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Entdeckte Kontakte suchen';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Kontakt hinzugefügt';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Kontakt hinzufügen';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact =>
|
||||
'Kontakt in die Zwischenablage kopieren';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Kontakt löschen';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Alle entdeckten Kontakte löschen';
|
||||
|
||||
@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';
|
||||
|
||||
@override
|
||||
String get translation_title => 'Übersetzung';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Aktivieren Sie die Übersetzung';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Nachrichten empfangen und übersetzen sowie die Möglichkeit bieten, Nachrichten vor dem Versenden zu übersetzen.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Übersetzen Sie vor dem Versenden';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Steuert den Standardzustand des Icons für die Übersetzung des Komponisten.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Zielsprache';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => 'Verwenden Sie die App-Sprache';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Heruntergeladenes Modell';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel =>
|
||||
'Vordefinierter Hugging Face-Modell';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => 'URL für das manuelle Modell';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Modell herunterladen';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Herunterladen...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Arbeiten...';
|
||||
|
||||
@override
|
||||
String get translation_stop => 'Stopp';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Zusammenführen der heruntergeladenen Teile in die finale Datei...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Heruntergeladene Modelle';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Modell löschen';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded =>
|
||||
'Übersetzungsmotor heruntergeladen.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'Herunterladen abgebrochen.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'Download fehlgeschlagen: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst =>
|
||||
'Geben Sie zunächst die URL eines Modells ein.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'PIN anzeigen';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingHidePin => 'PIN ausblenden';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingPinTitle => 'Bluetooth-Paarungs-PIN';
|
||||
|
||||
@override
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Geben Sie die PIN für $deviceName ein (leer lassen, falls keine).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Nachricht übersetzen';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending =>
|
||||
'Übersetzen Sie vor dem Versenden';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'Die Nachrichten werden vor dem Versenden übersetzt.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Nachrichten in der ursprünglichen, getippten Sprache senden.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Übersetzen Sie auf $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Übersetzungsmöglichkeiten';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Sprache des Systems';
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get common_delete => 'Delete';
|
||||
|
||||
@override
|
||||
String get common_deleteAll => 'Delete All';
|
||||
|
||||
@override
|
||||
String get common_close => 'Close';
|
||||
|
||||
@@ -108,6 +111,132 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTcpLabel => 'TCP';
|
||||
|
||||
@override
|
||||
String get tcpScreenTitle => 'Connect over TCP';
|
||||
|
||||
@override
|
||||
String get tcpHostLabel => 'IP Address';
|
||||
|
||||
@override
|
||||
String get tcpHostHint => '192.168.40.10';
|
||||
|
||||
@override
|
||||
String get tcpPortLabel => 'Port';
|
||||
|
||||
@override
|
||||
String get tcpPortHint => '5000';
|
||||
|
||||
@override
|
||||
String get tcpStatus_notConnected => 'Enter endpoint and connect';
|
||||
|
||||
@override
|
||||
String tcpStatus_connectingTo(String endpoint) {
|
||||
return 'Connecting to $endpoint...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tcpErrorHostRequired => 'IP address is required.';
|
||||
|
||||
@override
|
||||
String get tcpErrorPortInvalid => 'Port must be between 1 and 65535.';
|
||||
|
||||
@override
|
||||
String get tcpErrorUnsupported =>
|
||||
'TCP transport is not supported on this platform.';
|
||||
|
||||
@override
|
||||
String get tcpErrorTimedOut => 'TCP connection timed out.';
|
||||
|
||||
@override
|
||||
String tcpConnectionFailed(String error) {
|
||||
return 'TCP connection failed: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Connect over USB';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Choose a detected serial device and connect directly to your MeshCore node.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Select a USB device';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'USB serial is active on supported Android devices and desktop platforms.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'No USB devices found. Plug one in and refresh.';
|
||||
|
||||
@override
|
||||
String get usbErrorPermissionDenied => 'USB permission was denied.';
|
||||
|
||||
@override
|
||||
String get usbErrorDeviceMissing =>
|
||||
'The selected USB device is no longer available.';
|
||||
|
||||
@override
|
||||
String get usbErrorInvalidPort => 'Select a valid USB device.';
|
||||
|
||||
@override
|
||||
String get usbErrorBusy =>
|
||||
'Another USB connection request is already in progress.';
|
||||
|
||||
@override
|
||||
String get usbErrorNotConnected => 'No USB device is connected.';
|
||||
|
||||
@override
|
||||
String get usbErrorOpenFailed => 'Failed to open the selected USB device.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectFailed =>
|
||||
'Failed to connect to the selected USB device.';
|
||||
|
||||
@override
|
||||
String get usbErrorUnsupported =>
|
||||
'USB serial is not supported on this platform.';
|
||||
|
||||
@override
|
||||
String get usbErrorAlreadyActive => 'A USB connection is already active.';
|
||||
|
||||
@override
|
||||
String get usbErrorNoDeviceSelected => 'No USB device was selected.';
|
||||
|
||||
@override
|
||||
String get usbErrorPortClosed => 'The USB connection is not open.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectTimedOut =>
|
||||
'Connection timed out. Make sure the device has USB Companion firmware.';
|
||||
|
||||
@override
|
||||
String get usbFallbackDeviceName => 'Web Serial Device';
|
||||
|
||||
@override
|
||||
String get usbStatus_notConnected => 'Select a USB device';
|
||||
|
||||
@override
|
||||
String get usbStatus_connecting => 'Connecting to USB device...';
|
||||
|
||||
@override
|
||||
String get usbStatus_searching => 'Searching for USB devices...';
|
||||
|
||||
@override
|
||||
String usbConnectionFailed(String error) {
|
||||
return 'USB connection failed: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Scanning for devices...';
|
||||
|
||||
@@ -149,6 +278,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Please turn on Bluetooth to scan for devices';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Chrome Browser Required';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'This web application requires Google Chrome or a Chromium-based browser for Bluetooth support.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Enable Bluetooth';
|
||||
|
||||
@@ -232,6 +368,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get settings_longitude => 'Longitude';
|
||||
|
||||
@override
|
||||
String get settings_contactSettings => 'Contact Settings';
|
||||
|
||||
@override
|
||||
String get settings_contactSettingsSubtitle =>
|
||||
'Settings for how contacts are added.';
|
||||
|
||||
@override
|
||||
String get settings_privacyMode => 'Privacy Mode';
|
||||
|
||||
@@ -249,6 +392,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';
|
||||
|
||||
@@ -541,6 +726,48 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get appSettings_autoRouteRotationDisabled =>
|
||||
'Auto route rotation disabled';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeight => 'Max Route Weight';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeightSubtitle =>
|
||||
'Maximum weight a path can accumulate from successful deliveries';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeight => 'Initial Route Weight';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeightSubtitle =>
|
||||
'Starting weight for newly discovered paths';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrement =>
|
||||
'Success Weight Increment';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||
'Weight added to a path after successful delivery';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrement =>
|
||||
'Failure Weight Decrement';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||
'Weight removed from a path after failed delivery';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetries => 'Max Message Retries';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Number of retry attempts before marking a message as failed';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Battery';
|
||||
|
||||
@@ -746,6 +973,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'Group name is required';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => 'This group name is reserved';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'Group \"$name\" already exists';
|
||||
@@ -761,29 +991,63 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get contacts_noMembers => 'No members';
|
||||
|
||||
@override
|
||||
String get contacts_lastSeenNow => 'Last seen now';
|
||||
String get contacts_lastSeenNow => 'recently';
|
||||
|
||||
@override
|
||||
String contacts_lastSeenMinsAgo(int minutes) {
|
||||
return 'Last seen $minutes mins ago';
|
||||
return '~ $minutes min.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_lastSeenHourAgo => 'Last seen 1 hour ago';
|
||||
String get contacts_lastSeenHourAgo => '~ 1 hour';
|
||||
|
||||
@override
|
||||
String contacts_lastSeenHoursAgo(int hours) {
|
||||
return 'Last seen $hours hours ago';
|
||||
return '~ $hours hours';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_lastSeenDayAgo => 'Last seen 1 day ago';
|
||||
String get contacts_lastSeenDayAgo => '~ 1 day';
|
||||
|
||||
@override
|
||||
String contacts_lastSeenDaysAgo(int days) {
|
||||
return 'Last seen $days days ago';
|
||||
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';
|
||||
|
||||
@@ -949,6 +1213,14 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'No messages yet';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Send message';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Send a message to $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => 'Send a message to get started';
|
||||
|
||||
@@ -968,11 +1240,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Location';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Send a message to $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Type a message...';
|
||||
|
||||
@@ -1344,6 +1611,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Share marker here';
|
||||
|
||||
@override
|
||||
String get map_setAsMyLocation => 'Set as my location';
|
||||
|
||||
@override
|
||||
String get map_pinLabel => 'Pin label';
|
||||
|
||||
@@ -1389,6 +1659,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';
|
||||
|
||||
@@ -1404,6 +1677,15 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get map_showSharedMarkers => 'Show shared markers';
|
||||
|
||||
@override
|
||||
String get map_showGuessedLocations => 'Show guessed node locations';
|
||||
|
||||
@override
|
||||
String get map_showDiscoveryContacts => 'Show Discovery Contacts';
|
||||
|
||||
@override
|
||||
String get map_guessedLocation => 'Guessed location';
|
||||
|
||||
@override
|
||||
String get map_lastSeenTime => 'Last Seen Time';
|
||||
|
||||
@@ -1420,7 +1702,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';
|
||||
@@ -1586,11 +1871,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get login_repeaterDescription =>
|
||||
'Enter the repeater password to access settings and status.';
|
||||
'Enter the repeater password for guest or admin access.';
|
||||
|
||||
@override
|
||||
String get login_roomDescription =>
|
||||
'Enter the room password to access settings and status.';
|
||||
'Enter the room password for guest or admin access.';
|
||||
|
||||
@override
|
||||
String get login_routing => 'Routing';
|
||||
@@ -1694,9 +1979,18 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'Room Server Management';
|
||||
|
||||
@override
|
||||
String get repeater_guest => 'Repeater Information';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Room Server Information';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Management Tools';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Guest Tools';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Status';
|
||||
|
||||
@@ -1729,6 +2023,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_settingsSubtitle => 'Configure repeater parameters';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin => 'Clock sync after login';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Automatically send \"clock sync\" after a successful login';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'Repeater Status';
|
||||
|
||||
@@ -2097,6 +2398,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Clock';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'Clock Sync';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Discover Neighbors';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Sends an advertisement packet';
|
||||
|
||||
@@ -3068,4 +3375,265 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Last seen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Contacts settings';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Automatic Discovery';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle => 'Other contact related settings';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle => 'Auto-add users';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Allow the companion to automatically add discovered users.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Allow the companion to automatically add discovered repeaters.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Auto-add room servers';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Allow the companion to automatically add discovered room servers.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Allow the companion to automatically add discovered sensors.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'When the contact list is full, the oldest non-favorited contact will be replaced.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Discovered Contacts';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => 'No matching contacts';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Search discovered contacts';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Contact added';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Add Contact';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact => 'Copy Contact to clipboard';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Delete Discovered Contact';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Delete All Discovered Contacts';
|
||||
|
||||
@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';
|
||||
|
||||
@override
|
||||
String get translation_title => 'Translation';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Enable translation';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Translate incoming messages and allow pre-send translation.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Translate before sending';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Controls the default state of the composer translation icon.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Target language';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => 'Use app language';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Downloaded model';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel => 'Preset Hugging Face model';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => 'Manual model URL';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Download model';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Downloading...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Working...';
|
||||
|
||||
@override
|
||||
String get translation_stop => 'Stop';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Merging downloaded chunks into final file...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Downloaded models';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Delete model';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded => 'Translation model downloaded.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'Download stopped.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'Download failed: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst => 'Enter a model URL first.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Show PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingHidePin => 'Hide PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingPinTitle => 'Bluetooth Pairing PIN';
|
||||
|
||||
@override
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Enter PIN for $deviceName (leave blank if none).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Message translation';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending => 'Translate before sending';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'Messages will be translated before send.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Send messages in the original typed language.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Translate to $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Translation options';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'System language';
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get common_delete => 'Eliminar';
|
||||
|
||||
@override
|
||||
String get common_deleteAll => 'Eliminar todo';
|
||||
|
||||
@override
|
||||
String get common_close => 'Cerrar';
|
||||
|
||||
@@ -108,6 +111,135 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTcpLabel => 'TCP';
|
||||
|
||||
@override
|
||||
String get tcpScreenTitle => 'Establecer conexión a través de TCP';
|
||||
|
||||
@override
|
||||
String get tcpHostLabel => 'Dirección IP';
|
||||
|
||||
@override
|
||||
String get tcpHostHint => '192.168.40.10';
|
||||
|
||||
@override
|
||||
String get tcpPortLabel => 'Puerto';
|
||||
|
||||
@override
|
||||
String get tcpPortHint => '5000';
|
||||
|
||||
@override
|
||||
String get tcpStatus_notConnected => 'Ingrese la dirección final y conecte.';
|
||||
|
||||
@override
|
||||
String tcpStatus_connectingTo(String endpoint) {
|
||||
return 'Conectándose a $endpoint...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tcpErrorHostRequired => 'Se requiere la dirección IP.';
|
||||
|
||||
@override
|
||||
String get tcpErrorPortInvalid => 'El puerto debe estar entre 1 y 65535.';
|
||||
|
||||
@override
|
||||
String get tcpErrorUnsupported =>
|
||||
'El protocolo de transporte TCP no está soportado en esta plataforma.';
|
||||
|
||||
@override
|
||||
String get tcpErrorTimedOut => 'La conexión TCP ha caducado.';
|
||||
|
||||
@override
|
||||
String tcpConnectionFailed(String error) {
|
||||
return 'Error en la conexión TCP: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Conecte mediante USB';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Seleccione el dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Seleccione un dispositivo USB';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'No se encontraron dispositivos USB. Conecte uno y vuelva a intentar.';
|
||||
|
||||
@override
|
||||
String get usbErrorPermissionDenied =>
|
||||
'Se denegó el permiso de acceso a través de USB.';
|
||||
|
||||
@override
|
||||
String get usbErrorDeviceMissing =>
|
||||
'El dispositivo USB seleccionado ya no está disponible.';
|
||||
|
||||
@override
|
||||
String get usbErrorInvalidPort => 'Seleccione un dispositivo USB válido.';
|
||||
|
||||
@override
|
||||
String get usbErrorBusy =>
|
||||
'Ya se ha iniciado una solicitud de conexión USB adicional.';
|
||||
|
||||
@override
|
||||
String get usbErrorNotConnected => 'No hay ningún dispositivo USB conectado.';
|
||||
|
||||
@override
|
||||
String get usbErrorOpenFailed =>
|
||||
'No se pudo abrir el dispositivo USB seleccionado.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectFailed =>
|
||||
'No se pudo conectar con el dispositivo USB seleccionado.';
|
||||
|
||||
@override
|
||||
String get usbErrorUnsupported =>
|
||||
'La comunicación serial a través de USB no está soportada en esta plataforma.';
|
||||
|
||||
@override
|
||||
String get usbErrorAlreadyActive => 'La conexión USB ya está activa.';
|
||||
|
||||
@override
|
||||
String get usbErrorNoDeviceSelected =>
|
||||
'No se ha seleccionado ningún dispositivo USB.';
|
||||
|
||||
@override
|
||||
String get usbErrorPortClosed => 'La conexión USB no está activa.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectTimedOut =>
|
||||
'La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion.';
|
||||
|
||||
@override
|
||||
String get usbFallbackDeviceName => 'Dispositivo de serie web';
|
||||
|
||||
@override
|
||||
String get usbStatus_notConnected => 'Seleccione un dispositivo USB';
|
||||
|
||||
@override
|
||||
String get usbStatus_connecting => 'Conectándose al dispositivo USB...';
|
||||
|
||||
@override
|
||||
String get usbStatus_searching => 'Buscando dispositivos USB...';
|
||||
|
||||
@override
|
||||
String usbConnectionFailed(String error) {
|
||||
return 'Error al conectar mediante USB: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Escaneando dispositivos...';
|
||||
|
||||
@@ -150,6 +282,13 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Por favor, active el Bluetooth para escanear dispositivos.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Navegador Chrome requerido';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Habilitar Bluetooth';
|
||||
|
||||
@@ -233,6 +372,13 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get settings_longitude => 'Longitud';
|
||||
|
||||
@override
|
||||
String get settings_contactSettings => 'Configuración de contacto';
|
||||
|
||||
@override
|
||||
String get settings_contactSettingsSubtitle =>
|
||||
'Configuración de cómo se agregan los contactos.';
|
||||
|
||||
@override
|
||||
String get settings_privacyMode => 'Modo Privacidad';
|
||||
|
||||
@@ -250,6 +396,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';
|
||||
|
||||
@@ -548,6 +739,49 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get appSettings_autoRouteRotationDisabled =>
|
||||
'Rotación de ruta automática desactivada';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeight => 'Peso máximo permitido para la ruta';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeightSubtitle =>
|
||||
'Peso máximo que una ruta puede acumular gracias a entregas exitosas.';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeight => 'Peso inicial de la ruta';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeightSubtitle =>
|
||||
'Peso inicial para rutas recién descubiertas';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrement =>
|
||||
'Incremento de peso para el éxito';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||
'Peso añadido a una ruta después de una entrega exitosa.';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrement =>
|
||||
'Reducción del peso asociado al fallo';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||
'Peso retirado de un camino después de un intento de entrega fallido.';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetries =>
|
||||
'Número máximo de reintentos de envío de mensajes';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Número de intentos de reintento antes de marcar un mensaje como fallido.';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Batería';
|
||||
|
||||
@@ -755,6 +989,10 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'El nombre del grupo es obligatorio';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved =>
|
||||
'Este nombre de grupo está reservado';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'El grupo \"$name\" ya existe';
|
||||
@@ -775,25 +1013,61 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String contacts_lastSeenMinsAgo(int minutes) {
|
||||
return 'Última vez visto hace $minutes minutos.';
|
||||
return '~ $minutes min.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_lastSeenHourAgo => 'Última vez que se vio hace 1 hora';
|
||||
String get contacts_lastSeenHourAgo => '~ 1 hora';
|
||||
|
||||
@override
|
||||
String contacts_lastSeenHoursAgo(int hours) {
|
||||
return 'Última vez visto hace $hours horas.';
|
||||
return '~ $hours horas';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_lastSeenDayAgo => 'Última vez que se vio hace 1 día';
|
||||
String get contacts_lastSeenDayAgo => '~ 1 día';
|
||||
|
||||
@override
|
||||
String contacts_lastSeenDaysAgo(int days) {
|
||||
return 'Última vez visto hace $days días.';
|
||||
return '~ $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Información de contacto';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Configuración de contacto';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Telemetría';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Visto por última vez';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Borrar chat';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Base de Telemetría';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Permitir el intercambio de nivel de batería y telemetría básica';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Ubicación de telemetría';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle =>
|
||||
'Permitir el intercambio de datos de ubicación';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Entorno de Telemetría';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle =>
|
||||
'Permitir el intercambio de datos de sensores de entorno';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Canales';
|
||||
|
||||
@@ -964,6 +1238,14 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'Aún no hay mensajes';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Enviar mensaje';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Enviar un mensaje a $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => 'Enviar un mensaje para comenzar';
|
||||
|
||||
@@ -983,11 +1265,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Ubicación';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Enviar un mensaje a $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Escribe un mensaje...';
|
||||
|
||||
@@ -1363,6 +1640,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Compartir marcador aquí';
|
||||
|
||||
@override
|
||||
String get map_setAsMyLocation => 'Establecer mi ubicación';
|
||||
|
||||
@override
|
||||
String get map_pinLabel => 'Etiqueta de marcador';
|
||||
|
||||
@@ -1408,6 +1688,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';
|
||||
|
||||
@@ -1423,6 +1706,16 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get map_showSharedMarkers => 'Mostrar marcadores compartidos';
|
||||
|
||||
@override
|
||||
String get map_showGuessedLocations =>
|
||||
'Mostrar las ubicaciones estimadas de los nodos.';
|
||||
|
||||
@override
|
||||
String get map_showDiscoveryContacts => 'Mostrar Contactos de Descubrimiento';
|
||||
|
||||
@override
|
||||
String get map_guessedLocation => 'Ubicación estimada';
|
||||
|
||||
@override
|
||||
String get map_lastSeenTime => 'Última vez que se vio';
|
||||
|
||||
@@ -1441,6 +1734,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';
|
||||
|
||||
@@ -1719,9 +2015,18 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'Administración del Servidor de Habitación';
|
||||
|
||||
@override
|
||||
String get repeater_guest => 'Información sobre repetidores';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Información del servidor';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Herramientas de Gestión';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Herramientas para invitados';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Estado';
|
||||
|
||||
@@ -1754,6 +2059,14 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_settingsSubtitle => 'Configurar parámetros del repetidor';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin =>
|
||||
'Sincronización del reloj después de iniciar sesión';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Enviar automáticamente la función de \"sincronización de reloj\" después de un inicio de sesión exitoso.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'Estado del Repetidor';
|
||||
|
||||
@@ -2130,6 +2443,12 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Reloj';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'Sincronización del reloj';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Descubrir Vecinos';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Envía un paquete de publicidad';
|
||||
|
||||
@@ -3116,4 +3435,278 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Visto por última vez';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Configuración de contactos';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Detección automática';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Otras configuraciones relacionadas con el contacto';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Agregar usuarios automáticamente';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Permitir que el compañero agregue automáticamente a los usuarios descubiertos.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Agregar repetidores automáticamente';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Permitir que el compañero agregue automáticamente los repetidores descubiertos.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Agregar automáticamente servidores de sala';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Permitir que el compañero agregue automáticamente los servidores de salas descubiertos.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Agregar sensores automáticamente';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Permitir que el compañero agregue automáticamente los sensores descubiertos.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle =>
|
||||
'Sobreescribir el más antiguo';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'Cuando la lista de contactos esté llena, se reemplazará el contacto no favorito más antiguo.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Contactos descubiertos';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching =>
|
||||
'No se encontraron contactos coincidentes';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Buscar contactos descubiertos';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Contacto agregado';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Agregar contacto';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact =>
|
||||
'Copiar contacto al portapapeles';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Eliminar contacto';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Eliminar Todos los Contactos Descubiertos';
|
||||
|
||||
@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';
|
||||
|
||||
@override
|
||||
String get translation_title => 'Traducción';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Habilitar la traducción';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Traducir los mensajes entrantes y permitir la traducción previa al envío.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Traducir antes de enviar';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Controla el estado predeterminado del icono de traducción del compositor.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Idioma de destino';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage =>
|
||||
'Utilizar el idioma de la aplicación';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Modelo descargado';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel =>
|
||||
'Modelo predefinido de Hugging Face';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => 'URL del modelo manual';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Descargar el modelo';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Descargando...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Trabajando...';
|
||||
|
||||
@override
|
||||
String get translation_stop => '¡Detente!';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Combinando los fragmentos descargados en el archivo final...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Modelos descargados';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Eliminar modelo';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded => 'Modelo de traducción descargado.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'La descarga se ha detenido.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'No se pudo descargar: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst =>
|
||||
'Primero, introduzca la URL del modelo.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Mostrar código PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingHidePin => 'Ocultar PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingPinTitle =>
|
||||
'PIN para emparejar dispositivos Bluetooth';
|
||||
|
||||
@override
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Introduzca el código PIN para $deviceName (deje en blanco si no hay ninguno).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Traducción del mensaje';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending => 'Traducir antes de enviar';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'Los mensajes serán traducidos antes de ser enviados.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Envía mensajes utilizando el lenguaje escrito original.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Traducir a $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Opciones de traducción';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Idioma del sistema';
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get common_delete => 'Supprimer';
|
||||
|
||||
@override
|
||||
String get common_deleteAll => 'Supprimer tout';
|
||||
|
||||
@override
|
||||
String get common_close => 'Fermer';
|
||||
|
||||
@@ -108,6 +111,137 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTcpLabel => 'TCP';
|
||||
|
||||
@override
|
||||
String get tcpScreenTitle => 'Établir une connexion via TCP';
|
||||
|
||||
@override
|
||||
String get tcpHostLabel => 'Adresse IP';
|
||||
|
||||
@override
|
||||
String get tcpHostHint => '192.168.40.10';
|
||||
|
||||
@override
|
||||
String get tcpPortLabel => 'Port';
|
||||
|
||||
@override
|
||||
String get tcpPortHint => '5000';
|
||||
|
||||
@override
|
||||
String get tcpStatus_notConnected =>
|
||||
'Entrez l\'adresse de destination et connectez-vous.';
|
||||
|
||||
@override
|
||||
String tcpStatus_connectingTo(String endpoint) {
|
||||
return 'Connexion à $endpoint...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tcpErrorHostRequired => 'Une adresse IP est obligatoire.';
|
||||
|
||||
@override
|
||||
String get tcpErrorPortInvalid =>
|
||||
'La taille du port doit être comprise entre 1 et 65535.';
|
||||
|
||||
@override
|
||||
String get tcpErrorUnsupported =>
|
||||
'Le protocole TCP n\'est pas pris en charge sur cette plateforme.';
|
||||
|
||||
@override
|
||||
String get tcpErrorTimedOut => 'La connexion TCP a expiré.';
|
||||
|
||||
@override
|
||||
String tcpConnectionFailed(String error) {
|
||||
return 'Échec de la connexion TCP : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Connectez via USB';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Sélectionnez un périphérique série détecté et connectez-vous directement à votre nœud MeshCore.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Sélectionnez un périphérique USB';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'La communication série USB est active sur les appareils Android et les plateformes de bureau compatibles.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Aucun périphérique USB n\'a été trouvé. Veuillez en brancher un et rafraîchir la page.';
|
||||
|
||||
@override
|
||||
String get usbErrorPermissionDenied => 'L\'accès via USB a été refusé.';
|
||||
|
||||
@override
|
||||
String get usbErrorDeviceMissing =>
|
||||
'Le périphérique USB sélectionné n\'est plus disponible.';
|
||||
|
||||
@override
|
||||
String get usbErrorInvalidPort => 'Sélectionnez un périphérique USB valide.';
|
||||
|
||||
@override
|
||||
String get usbErrorBusy =>
|
||||
'Une autre demande de connexion USB est déjà en cours.';
|
||||
|
||||
@override
|
||||
String get usbErrorNotConnected => 'Aucun appareil USB n\'est connecté.';
|
||||
|
||||
@override
|
||||
String get usbErrorOpenFailed =>
|
||||
'Impossible d\'ouvrir l\'appareil USB sélectionné.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectFailed =>
|
||||
'Impossible de se connecter à l\'appareil USB sélectionné.';
|
||||
|
||||
@override
|
||||
String get usbErrorUnsupported =>
|
||||
'La communication série USB n\'est pas prise en charge sur cette plateforme.';
|
||||
|
||||
@override
|
||||
String get usbErrorAlreadyActive => 'Une connexion USB est déjà établie.';
|
||||
|
||||
@override
|
||||
String get usbErrorNoDeviceSelected =>
|
||||
'Aucun appareil USB n\'a été sélectionné.';
|
||||
|
||||
@override
|
||||
String get usbErrorPortClosed => 'La connexion USB n\'est pas établie.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectTimedOut =>
|
||||
'La connexion a expiré. Assurez-vous que l\'appareil dispose du firmware USB Companion.';
|
||||
|
||||
@override
|
||||
String get usbFallbackDeviceName =>
|
||||
'Dispositif de communication série sur le Web';
|
||||
|
||||
@override
|
||||
String get usbStatus_notConnected => 'Sélectionnez un périphérique USB';
|
||||
|
||||
@override
|
||||
String get usbStatus_connecting => 'Connexion au périphérique USB...';
|
||||
|
||||
@override
|
||||
String get usbStatus_searching => 'Recherche de périphériques USB...';
|
||||
|
||||
@override
|
||||
String usbConnectionFailed(String error) {
|
||||
return 'Échec de la connexion USB : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Recherche de périphériques...';
|
||||
|
||||
@@ -150,6 +284,13 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Veuillez activer le Bluetooth pour rechercher des appareils.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Navigateur Chrome requis';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Cette application web nécessite Google Chrome ou un navigateur basé sur Chromium pour le support Bluetooth.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Activer le Bluetooth';
|
||||
|
||||
@@ -234,6 +375,13 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get settings_longitude => 'Longitude';
|
||||
|
||||
@override
|
||||
String get settings_contactSettings => 'Paramètres de contact';
|
||||
|
||||
@override
|
||||
String get settings_contactSettingsSubtitle =>
|
||||
'Paramètres pour l\'ajout de contacts';
|
||||
|
||||
@override
|
||||
String get settings_privacyMode => 'Mode de confidentialité';
|
||||
|
||||
@@ -252,6 +400,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';
|
||||
|
||||
@@ -365,10 +559,10 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get settings_bandwidth => 'Bande passante';
|
||||
|
||||
@override
|
||||
String get settings_spreadingFactor => 'Facteur de répartition';
|
||||
String get settings_spreadingFactor => 'Facteur de répartition (SF)';
|
||||
|
||||
@override
|
||||
String get settings_codingRate => 'Taux de codage';
|
||||
String get settings_codingRate => 'Taux de codage (CR)';
|
||||
|
||||
@override
|
||||
String get settings_txPower => 'TX Puissance (dBm)';
|
||||
@@ -550,6 +744,50 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get appSettings_autoRouteRotationDisabled =>
|
||||
'Rotation de l\'itinéraire automatique désactivée';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeight =>
|
||||
'Poids maximal autorisé pour le trajet';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeightSubtitle =>
|
||||
'Poids maximal qu\'un itinéraire peut accumuler grâce à des livraisons réussies.';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeight => 'Poids initial de l\'itinéraire';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeightSubtitle =>
|
||||
'Poids de départ pour les nouveaux chemins découverts';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrement =>
|
||||
'Augmentation du poids de réussite';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||
'Poids ajouté à un itinéraire après une livraison réussie.';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrement =>
|
||||
'Réduction du poids de pénalité';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||
'Poids retiré d\'un itinéraire après une tentative de livraison infructueuse.';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetries =>
|
||||
'Nombre maximal de tentatives de récupération de messages';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Nombre de tentatives de relance avant de marquer un message comme ayant échoué.';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Batterie';
|
||||
|
||||
@@ -708,7 +946,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String contacts_searchRoomServers(int number, String str) {
|
||||
return 'Rechercher $number$str serveurs de salle...';
|
||||
return 'Rechercher $number$str room server...';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -757,6 +995,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'Le nom du groupe est obligatoire.';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => 'Ce nom de groupe est réservé';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'Le groupe \"$name\" existe déjà.';
|
||||
@@ -777,25 +1018,61 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String contacts_lastSeenMinsAgo(int minutes) {
|
||||
return 'Vu il y a $minutes minutes';
|
||||
return '~ $minutes min.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_lastSeenHourAgo => 'Vu il y a 1 heure';
|
||||
String get contacts_lastSeenHourAgo => '~ 1 heure';
|
||||
|
||||
@override
|
||||
String contacts_lastSeenHoursAgo(int hours) {
|
||||
return 'Vu il y a $hours heures';
|
||||
return '~ $hours heures';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_lastSeenDayAgo => 'Vu il y a 1 jour';
|
||||
String get contacts_lastSeenDayAgo => '~ 1 jour';
|
||||
|
||||
@override
|
||||
String contacts_lastSeenDaysAgo(int days) {
|
||||
return 'Vu il y a $days jours';
|
||||
return '~ $days jours';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Informations de contact';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Paramètres de contact';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Télémétrie';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Dernière fois vu';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Effacer la conversation';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Base de télémétrie';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Autoriser le partage du niveau de batterie et de la télémétrie de base';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Emplacement de télémétrie';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle =>
|
||||
'Autoriser le partage des données de localisation';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Environnement Télémétrie';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle =>
|
||||
'Autoriser le partage des données des capteurs d\'environnement';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Canaux';
|
||||
|
||||
@@ -952,7 +1229,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
'N\'importe qui peut rejoindre les canaux #hashtag.';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCode => 'Scanner un code QR';
|
||||
String get channels_scanQrCode => 'Scanner un QR code';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCodeComingSoon => 'Bientôt disponible';
|
||||
@@ -966,6 +1243,14 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'Aucun message pour le moment.';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Envoyer un message';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Envoyer un message à $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => 'Envoyer un message pour commencer';
|
||||
|
||||
@@ -985,11 +1270,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Emplacement';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Envoyer un message à $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Saisir un message...';
|
||||
|
||||
@@ -1212,7 +1492,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get chat_floodModeSubtitle =>
|
||||
'Utiliser le commutateur de routage dans la barre d\'application';
|
||||
'Désactive l\'apprentissage du chemin (à éviter). Utiliser le commutateur de routage dans la barre d\'application pour rebasculer en mode auto par la suite.';
|
||||
|
||||
@override
|
||||
String get chat_floodModeEnabled =>
|
||||
@@ -1337,7 +1617,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get map_repeater => 'Répéteur';
|
||||
|
||||
@override
|
||||
String get map_room => 'Salle';
|
||||
String get map_room => 'Room Server';
|
||||
|
||||
@override
|
||||
String get map_sensor => 'Capteur';
|
||||
@@ -1370,6 +1650,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Partager le marqueur ici';
|
||||
|
||||
@override
|
||||
String get map_setAsMyLocation => 'Définir comme ma localisation';
|
||||
|
||||
@override
|
||||
String get map_pinLabel => 'Étiquete de repin';
|
||||
|
||||
@@ -1415,6 +1698,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é';
|
||||
|
||||
@@ -1430,6 +1716,16 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get map_showSharedMarkers => 'Afficher les marqueurs partagés';
|
||||
|
||||
@override
|
||||
String get map_showGuessedLocations =>
|
||||
'Afficher les emplacements des nœuds estimés';
|
||||
|
||||
@override
|
||||
String get map_showDiscoveryContacts => 'Afficher les contacts de découverte';
|
||||
|
||||
@override
|
||||
String get map_guessedLocation => 'Lieu deviné';
|
||||
|
||||
@override
|
||||
String get map_lastSeenTime => 'Dernière fois vu';
|
||||
|
||||
@@ -1437,7 +1733,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get map_sharedPin => 'Clé partagée';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Rejoindre la salle';
|
||||
String get map_joinRoom => 'Rejoindre le room server';
|
||||
|
||||
@override
|
||||
String get map_manageRepeater => 'Gérer le répéteur';
|
||||
@@ -1449,6 +1745,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';
|
||||
|
||||
@@ -1703,7 +2002,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get path_noRepeatersFound =>
|
||||
'Aucun répéteur ou serveur de salle n\'a été trouvé.';
|
||||
'Aucun répéteur ou room server n\'a été trouvé.';
|
||||
|
||||
@override
|
||||
String get path_customPathsRequire =>
|
||||
@@ -1727,9 +2026,18 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'Administrattion Room Server';
|
||||
|
||||
@override
|
||||
String get repeater_guest => 'Informations sur les répéteurs';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Informations sur le serveur';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Outils de Gestion';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Outils pour les invités';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'État';
|
||||
|
||||
@@ -1763,6 +2071,14 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get repeater_settingsSubtitle =>
|
||||
'Configurer les paramètres du répéteur';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin =>
|
||||
'Synchronisation de l\'horloge après la connexion';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Envoyer automatiquement une notification \"synchronisation de l\'heure\" après une connexion réussie.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'État du répéteur';
|
||||
|
||||
@@ -1913,10 +2229,10 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get repeater_bandwidth => 'Bande passante';
|
||||
|
||||
@override
|
||||
String get repeater_spreadingFactor => 'Facteur de répartition';
|
||||
String get repeater_spreadingFactor => 'Facteur de répartition (SF)';
|
||||
|
||||
@override
|
||||
String get repeater_codingRate => 'Taux de codage';
|
||||
String get repeater_codingRate => 'Taux de codage (CR)';
|
||||
|
||||
@override
|
||||
String get repeater_locationSettings => 'Paramètres de localisation';
|
||||
@@ -1939,7 +2255,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get repeater_features => 'Fonctionnalités';
|
||||
|
||||
@override
|
||||
String get repeater_packetForwarding => 'Transfert de paquets';
|
||||
String get repeater_packetForwarding => 'Mode répéteur';
|
||||
|
||||
@override
|
||||
String get repeater_packetForwardingSubtitle =>
|
||||
@@ -2146,6 +2462,12 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Horloge';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'Synchronisation de l\'horloge';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Découvrir les voisins';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Envoie un paquet d\'annonce';
|
||||
|
||||
@@ -2600,11 +2922,11 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_scanQr => 'Scanner la communauté QR';
|
||||
String get community_scanQr => 'Scanner un QR code de communauté';
|
||||
|
||||
@override
|
||||
String get community_scanInstructions =>
|
||||
'Pointez l\'appareil photo vers un code QR communautaire.';
|
||||
'Pointez l\'appareil photo vers un QR code de communauté.';
|
||||
|
||||
@override
|
||||
String get community_showQr => 'Afficher le QR Code';
|
||||
@@ -2644,7 +2966,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
'Les canaux hashtag de la communauté ne sont accessibles qu\'aux membres de la communauté';
|
||||
|
||||
@override
|
||||
String get community_invalidQrCode => 'Code QR de communauté non valide';
|
||||
String get community_invalidQrCode => 'QR code de communauté non valide';
|
||||
|
||||
@override
|
||||
String get community_alreadyMember => 'Déjà membre';
|
||||
@@ -2668,7 +2990,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get community_scanOrCreate =>
|
||||
'Scanner un code QR ou créer une communauté pour commencer';
|
||||
'Scanner un QR code ou créer une communauté pour commencer';
|
||||
|
||||
@override
|
||||
String get community_manageCommunities => 'Gérer les Communautés';
|
||||
@@ -2696,7 +3018,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String community_regenerateSecretConfirm(String name) {
|
||||
return 'Régénérer la clé secrète pour \"$name\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.';
|
||||
return 'Régénérer la clé secrète pour \"$name\" ? Tous les membres devront scanner le nouveau QR code pour continuer à communiquer.';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2717,7 +3039,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String community_scanToUpdateSecret(String name) {
|
||||
return 'Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"$name\"';
|
||||
return 'Scanner le nouveau QR code pour mettre à jour le mot de passe pour \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2968,11 +3290,10 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get contacts_repeaterPing => 'Pinguer le répéteur';
|
||||
|
||||
@override
|
||||
String get contacts_roomPathTrace =>
|
||||
'Traçage du chemin vers le serveur de la salle';
|
||||
String get contacts_roomPathTrace => 'Traçage du chemin vers le room server';
|
||||
|
||||
@override
|
||||
String get contacts_roomPing => 'Pinguer le serveur de la salle';
|
||||
String get contacts_roomPing => 'Pinguer le room server';
|
||||
|
||||
@override
|
||||
String get contacts_chatTraceRoute => 'Tracer le chemin';
|
||||
@@ -3078,7 +3399,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeaters =>
|
||||
'Exporter les répéteurs / serveur de salle au format GPX';
|
||||
'Exporter les répéteurs / room servers au format GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersSubtitle =>
|
||||
@@ -3116,7 +3437,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersRoom =>
|
||||
'Emplacements des serveurs de répéteur et de salle';
|
||||
'Emplacements des répéteurs et room servers';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportChat => 'Emplacements des compagnons';
|
||||
@@ -3138,4 +3459,278 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Dernière fois vu';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Paramètres des contacts';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Découverte automatique';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Autres paramètres liés aux contacts';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Ajouter automatiquement les utilisateurs';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Autoriser le compagnon à ajouter automatiquement les utilisateurs découverts';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Ajouter automatiquement les répéteurs';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Autoriser le compagnon à ajouter automatiquement les répéteurs découverts';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Ajouter automatiquement les room servers';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Autoriser le compagnon à ajouter automatiquement les room servers découverts';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Ajouter automatiquement les capteurs';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Autoriser le compagnon à ajouter automatiquement les capteurs découverts.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle => 'Écraser le plus ancien';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'Lorsque la liste de contacts est pleine, le contact le plus ancien non favori sera remplacé.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Contacts découverts';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => 'Aucun contact correspondant';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint =>
|
||||
'Rechercher des contacts découverts';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Contact ajouté';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Ajouter un contact';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact =>
|
||||
'Copier le contact dans le presse-papiers';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Supprimer le contact';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Supprimer tous les contacts découverts';
|
||||
|
||||
@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';
|
||||
|
||||
@override
|
||||
String get translation_title => 'Traduction';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Activer la traduction';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Traduire les messages entrants et permettre la traduction avant l\'envoi.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Traduire avant d\'envoyer';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Contrôle l\'état par défaut de l\'icône de traduction du composant.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Langue cible';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage =>
|
||||
'Utiliser la langue de l\'application';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Modèle téléchargé';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel => 'Modèle Hugging Face préconfiguré';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => 'URL du modèle manuel';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Télécharger le modèle';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Téléchargement...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Au travail...';
|
||||
|
||||
@override
|
||||
String get translation_stop => 'Arrêtez';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Fusion des fragments téléchargés dans le fichier final...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Modèles téléchargés';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Supprimer le modèle';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded => 'Modèle de traduction téléchargé.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped =>
|
||||
'Le téléchargement a été interrompu.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'Échec du téléchargement : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst => 'Entrez d\'abord l\'URL du modèle.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Afficher le code PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingHidePin => 'Masquer le code PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingPinTitle =>
|
||||
'Code PIN pour la connexion Bluetooth';
|
||||
|
||||
@override
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Entrez le code PIN pour $deviceName (laissez vide si nécessaire).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Traduction du message';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending => 'Traduire avant d\'envoyer';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'Les messages seront traduits avant d\'être envoyés.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Envoyez des messages dans la langue originale, telle que vous l\'avez tapée.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Traduire en $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Options de traduction';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Langue du système';
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get common_delete => 'Elimina';
|
||||
|
||||
@override
|
||||
String get common_deleteAll => 'Elimina tutto';
|
||||
|
||||
@override
|
||||
String get common_close => 'Chiudi';
|
||||
|
||||
@@ -108,6 +111,137 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTcpLabel => 'TCP';
|
||||
|
||||
@override
|
||||
String get tcpScreenTitle => 'Stabilire una connessione tramite TCP';
|
||||
|
||||
@override
|
||||
String get tcpHostLabel => 'Indirizzo IP';
|
||||
|
||||
@override
|
||||
String get tcpHostHint => '192.168.40.10';
|
||||
|
||||
@override
|
||||
String get tcpPortLabel => 'Porta';
|
||||
|
||||
@override
|
||||
String get tcpPortHint => '5000';
|
||||
|
||||
@override
|
||||
String get tcpStatus_notConnected => 'Inserisci l\'endpoint e connettiti.';
|
||||
|
||||
@override
|
||||
String tcpStatus_connectingTo(String endpoint) {
|
||||
return 'Connessione a $endpoint...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tcpErrorHostRequired => 'È necessario fornire un indirizzo IP.';
|
||||
|
||||
@override
|
||||
String get tcpErrorPortInvalid =>
|
||||
'La dimensione della porta deve essere compresa tra 1 e 65535.';
|
||||
|
||||
@override
|
||||
String get tcpErrorUnsupported =>
|
||||
'Il protocollo TCP non è supportato su questa piattaforma.';
|
||||
|
||||
@override
|
||||
String get tcpErrorTimedOut => 'La connessione TCP è scaduta.';
|
||||
|
||||
@override
|
||||
String tcpConnectionFailed(String error) {
|
||||
return 'Impossibile stabilire la connessione TCP: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Connessione tramite USB';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Seleziona il dispositivo seriale rilevato e connettilo direttamente al tuo nodo MeshCore.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Seleziona un dispositivo USB';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Nessun dispositivo USB rilevato. Collegare uno e aggiornare.';
|
||||
|
||||
@override
|
||||
String get usbErrorPermissionDenied =>
|
||||
'È stato negato l\'accesso tramite USB.';
|
||||
|
||||
@override
|
||||
String get usbErrorDeviceMissing =>
|
||||
'Il dispositivo USB selezionato non è più disponibile.';
|
||||
|
||||
@override
|
||||
String get usbErrorInvalidPort => 'Seleziona un dispositivo USB valido.';
|
||||
|
||||
@override
|
||||
String get usbErrorBusy =>
|
||||
'Un\'altra richiesta di connessione tramite USB è già in corso.';
|
||||
|
||||
@override
|
||||
String get usbErrorNotConnected => 'Non è collegato alcun dispositivo USB.';
|
||||
|
||||
@override
|
||||
String get usbErrorOpenFailed =>
|
||||
'Impossibile aprire il dispositivo USB selezionato.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectFailed =>
|
||||
'Impossibile connettersi al dispositivo USB selezionato.';
|
||||
|
||||
@override
|
||||
String get usbErrorUnsupported =>
|
||||
'La comunicazione seriale tramite USB non è supportata su questa piattaforma.';
|
||||
|
||||
@override
|
||||
String get usbErrorAlreadyActive => 'La connessione USB è già attiva.';
|
||||
|
||||
@override
|
||||
String get usbErrorNoDeviceSelected =>
|
||||
'Non è stato selezionato alcun dispositivo USB.';
|
||||
|
||||
@override
|
||||
String get usbErrorPortClosed => 'La connessione USB non è attiva.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectTimedOut =>
|
||||
'La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion.';
|
||||
|
||||
@override
|
||||
String get usbFallbackDeviceName =>
|
||||
'Dispositivo per comunicazione seriale su rete';
|
||||
|
||||
@override
|
||||
String get usbStatus_notConnected => 'Seleziona un dispositivo USB';
|
||||
|
||||
@override
|
||||
String get usbStatus_connecting => 'Connessione al dispositivo USB...';
|
||||
|
||||
@override
|
||||
String get usbStatus_searching => 'Ricerca di dispositivi USB...';
|
||||
|
||||
@override
|
||||
String usbConnectionFailed(String error) {
|
||||
return 'Errore nella connessione USB: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Scansione in corso per i dispositivi...';
|
||||
|
||||
@@ -150,6 +284,13 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Browser Chrome richiesto';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Questa applicazione web richiede Google Chrome o un browser basato su Chromium per il supporto Bluetooth.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Abilita il Bluetooth';
|
||||
|
||||
@@ -233,6 +374,13 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get settings_longitude => 'Longitudine';
|
||||
|
||||
@override
|
||||
String get settings_contactSettings => 'Impostazioni di contatto';
|
||||
|
||||
@override
|
||||
String get settings_contactSettingsSubtitle =>
|
||||
'Impostazioni per l\'aggiunta dei contatti';
|
||||
|
||||
@override
|
||||
String get settings_privacyMode => 'Modalità Privacy';
|
||||
|
||||
@@ -250,6 +398,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';
|
||||
|
||||
@@ -547,6 +741,50 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get appSettings_autoRouteRotationDisabled =>
|
||||
'Rotazione del percorso automatico disabilitata';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeight =>
|
||||
'Massimo peso consentito per il percorso';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeightSubtitle =>
|
||||
'Il peso massimo che un percorso può accumulare grazie a consegne di successo.';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeight => 'Peso iniziale del percorso';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeightSubtitle =>
|
||||
'Peso di partenza per nuovi percorsi';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrement =>
|
||||
'Aumento del peso del successo';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||
'Peso aggiunto a un percorso dopo una consegna riuscita.';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrement =>
|
||||
'Riduzione del peso associato al fallimento';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||
'Peso rimosso da un percorso dopo un tentativo di consegna fallito.';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetries =>
|
||||
'Numero massimo di tentativi di invio del messaggio';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Numero di tentativi di riprova prima di considerare un messaggio come fallito.';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Batteria';
|
||||
|
||||
@@ -753,6 +991,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'Il nome del gruppo è obbligatorio.';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => 'Questo nome del gruppo è riservato';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'Il gruppo \"$name\" esiste già.';
|
||||
@@ -792,6 +1033,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';
|
||||
|
||||
@@ -962,6 +1239,14 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'Nessun messaggio ancora';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Invia messaggio';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Invia un messaggio a $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => 'Invia un messaggio per iniziare';
|
||||
|
||||
@@ -981,11 +1266,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Posizione';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Invia un messaggio a $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Digita un messaggio...';
|
||||
|
||||
@@ -1362,6 +1642,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Condividi marcatore qui';
|
||||
|
||||
@override
|
||||
String get map_setAsMyLocation => 'Imposta come la mia posizione';
|
||||
|
||||
@override
|
||||
String get map_pinLabel => 'Etichetta PIN';
|
||||
|
||||
@@ -1407,6 +1690,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';
|
||||
|
||||
@@ -1422,6 +1708,15 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get map_showSharedMarkers => 'Mostra i segnaposto condivisi';
|
||||
|
||||
@override
|
||||
String get map_showGuessedLocations => 'Mostra le posizioni stimate dei nodi';
|
||||
|
||||
@override
|
||||
String get map_showDiscoveryContacts => 'Mostra Contatti di Discovery';
|
||||
|
||||
@override
|
||||
String get map_guessedLocation => 'Località indovinata';
|
||||
|
||||
@override
|
||||
String get map_lastSeenTime => 'Ultimo Tempo di Visualizzazione';
|
||||
|
||||
@@ -1440,6 +1735,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';
|
||||
|
||||
@@ -1717,9 +2016,18 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'Gestione del Server di Camera';
|
||||
|
||||
@override
|
||||
String get repeater_guest => 'Informazioni sul ripetitore';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Informazioni sul server';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Strumenti di Gestione';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Strumenti per gli ospiti';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Stato';
|
||||
|
||||
@@ -1754,6 +2062,14 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get repeater_settingsSubtitle =>
|
||||
'Configura i parametri del ripetitore';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin =>
|
||||
'Sincronizzazione dell\'orologio dopo il login';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Invia automaticamente il comando \"sincronizzazione dell\'orologio\" dopo un login riuscito.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'Stato del Ripetitore';
|
||||
|
||||
@@ -2130,6 +2446,12 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Orologio';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'Sincronizzazione dell\'orologio';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Scopri i Vicini';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Invia un pacchetto pubblicitario';
|
||||
|
||||
@@ -3119,4 +3441,275 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Ultimo accesso';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Impostazioni dei contatti';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Scoperta automatica';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Altre impostazioni relative ai contatti';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Aggiungere utenti automaticamente';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Consenti al compagno di aggiungere automaticamente gli utenti scoperti.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Aggiungere ripetitori automaticamente';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Consenti al compagno di aggiungere automaticamente i ripetitori scoperti.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Aggiungere automaticamente i server delle stanze';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Consenti al compagno di aggiungere automaticamente i server delle stanze scoperte.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Aggiungere automaticamente i sensori';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Consenti al compagno di aggiungere automaticamente i sensori scoperti';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle =>
|
||||
'Sostituisci il più vecchio';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'Quando l\'elenco dei contatti è pieno, il contatto più vecchio non tra i preferiti verrà sostituito.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Contatti scoperti';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => 'Nessun contatto corrispondente';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Cerca contatti scoperti';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Contatto aggiunto';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Aggiungi contatto';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact => 'Copia contatto negli appunti';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Elimina Contatto';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Eliminare tutti i contatti scoperti';
|
||||
|
||||
@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';
|
||||
|
||||
@override
|
||||
String get translation_title => 'Traduzione';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Abilitare la traduzione';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Tradurre i messaggi in arrivo e consentire la traduzione preventiva prima dell\'invio.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Tradurre prima di inviare';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Controlla lo stato predefinito dell\'icona di traduzione del compositore.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Lingua di destinazione';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => 'Utilizza la lingua dell\'app';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Modello scaricato';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel =>
|
||||
'Modello predefinito di Hugging Face';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => 'URL del modello manuale';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Scarica il modello';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Inizio download...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Lavoro...';
|
||||
|
||||
@override
|
||||
String get translation_stop => 'Smetta';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Unione dei frammenti scaricati in un unico file...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Modelli scaricati';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Elimina modello';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded => 'Modello di traduzione scaricato.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'Il download è stato interrotto.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'Download fallito: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst =>
|
||||
'Inserite innanzitutto l\'URL del modello.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Mostra PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingHidePin => 'Nascondi il PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingPinTitle =>
|
||||
'PIN per l\'accoppiamento Bluetooth';
|
||||
|
||||
@override
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Inserire il codice PIN per $deviceName (lasciare vuoto se non presente).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Traduzione del messaggio';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending => 'Tradurre prima di inviare';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'I messaggi verranno tradotti prima di essere inviati.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Invia messaggi utilizzando la lingua originale, scritta.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Tradurre in $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Opzioni di traduzione';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Lingua del sistema';
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get common_delete => 'Verwijderen';
|
||||
|
||||
@override
|
||||
String get common_deleteAll => 'Alles verwijderen';
|
||||
|
||||
@override
|
||||
String get common_close => 'Sluiten';
|
||||
|
||||
@@ -108,6 +111,134 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTcpLabel => 'TCP';
|
||||
|
||||
@override
|
||||
String get tcpScreenTitle => 'Verbind via TCP';
|
||||
|
||||
@override
|
||||
String get tcpHostLabel => 'IP-adres';
|
||||
|
||||
@override
|
||||
String get tcpHostHint => '192.168.40.10';
|
||||
|
||||
@override
|
||||
String get tcpPortLabel => 'Poort';
|
||||
|
||||
@override
|
||||
String get tcpPortHint => '5000';
|
||||
|
||||
@override
|
||||
String get tcpStatus_notConnected => 'Voer het eindpunt in en verbind';
|
||||
|
||||
@override
|
||||
String tcpStatus_connectingTo(String endpoint) {
|
||||
return 'Verbinding maken met $endpoint...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tcpErrorHostRequired => 'Een IP-adres is vereist.';
|
||||
|
||||
@override
|
||||
String get tcpErrorPortInvalid =>
|
||||
'De poortwaarde moet tussen 1 en 65535 liggen.';
|
||||
|
||||
@override
|
||||
String get tcpErrorUnsupported =>
|
||||
'TCP-transport wordt niet ondersteund op deze platform.';
|
||||
|
||||
@override
|
||||
String get tcpErrorTimedOut => 'De TCP-verbinding is verlopen.';
|
||||
|
||||
@override
|
||||
String tcpConnectionFailed(String error) {
|
||||
return 'Verbinding met TCP mislukt: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Verbind via USB';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Selecteer een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Selecteer een USB-apparaat';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'USB-serieel is actief op ondersteunde Android-apparaten en desktop-platforms.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Geen USB-apparaten gevonden. Sluit er een aan en herlaad.';
|
||||
|
||||
@override
|
||||
String get usbErrorPermissionDenied => 'Toegang via USB is geweigerd.';
|
||||
|
||||
@override
|
||||
String get usbErrorDeviceMissing =>
|
||||
'Het geselecteerde USB-apparaat is niet meer beschikbaar.';
|
||||
|
||||
@override
|
||||
String get usbErrorInvalidPort => 'Selecteer een geldig USB-apparaat.';
|
||||
|
||||
@override
|
||||
String get usbErrorBusy =>
|
||||
'Een andere verzoek om een USB-verbinding is al in behandeling.';
|
||||
|
||||
@override
|
||||
String get usbErrorNotConnected => 'Er is geen USB-apparaat aangesloten.';
|
||||
|
||||
@override
|
||||
String get usbErrorOpenFailed =>
|
||||
'Kon het geselecteerde USB-apparaat niet openen.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectFailed =>
|
||||
'Kon niet verbinding maken met het geselecteerde USB-apparaat.';
|
||||
|
||||
@override
|
||||
String get usbErrorUnsupported =>
|
||||
'USB-serieel is niet ondersteund op deze platform.';
|
||||
|
||||
@override
|
||||
String get usbErrorAlreadyActive => 'Een USB-verbinding is al actief.';
|
||||
|
||||
@override
|
||||
String get usbErrorNoDeviceSelected => 'Geen USB-apparaat is geselecteerd.';
|
||||
|
||||
@override
|
||||
String get usbErrorPortClosed => 'De USB-verbinding is niet actief.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectTimedOut =>
|
||||
'Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft.';
|
||||
|
||||
@override
|
||||
String get usbFallbackDeviceName => 'Web-serieapparaat';
|
||||
|
||||
@override
|
||||
String get usbStatus_notConnected => 'Selecteer een USB-apparaat';
|
||||
|
||||
@override
|
||||
String get usbStatus_connecting => 'Verbinding maken met USB-apparaat...';
|
||||
|
||||
@override
|
||||
String get usbStatus_searching => 'Zoeken naar USB-apparaten...';
|
||||
|
||||
@override
|
||||
String usbConnectionFailed(String error) {
|
||||
return 'Fout bij de USB-verbinding: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Scannen naar apparaten...';
|
||||
|
||||
@@ -149,6 +280,13 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Chrome-browser vereist';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Deze webapplicatie vereist Google Chrome of een op Chromium gebaseerde browser voor Bluetooth-ondersteuning.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Activeer Bluetooth';
|
||||
|
||||
@@ -175,7 +313,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get settings_nodeSettings => 'Node Instellingen';
|
||||
|
||||
@override
|
||||
String get settings_nodeName => 'Node Naam';
|
||||
String get settings_nodeName => 'Nodenaam';
|
||||
|
||||
@override
|
||||
String get settings_nodeNameNotSet => 'Niet ingesteld';
|
||||
@@ -233,6 +371,13 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_longitude => 'Lengtegraad';
|
||||
|
||||
@override
|
||||
String get settings_contactSettings => 'Contactinstellingen';
|
||||
|
||||
@override
|
||||
String get settings_contactSettingsSubtitle =>
|
||||
'Instellingen voor het toevoegen van contacten';
|
||||
|
||||
@override
|
||||
String get settings_privacyMode => 'Privacy Mode';
|
||||
|
||||
@@ -250,6 +395,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';
|
||||
|
||||
@@ -263,7 +452,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get settings_advertisementSent => 'Advertentie verzonden';
|
||||
|
||||
@override
|
||||
String get settings_syncTime => 'Synchronisatie Tijd';
|
||||
String get settings_syncTime => 'Tijd Synchroniseren';
|
||||
|
||||
@override
|
||||
String get settings_syncTimeSubtitle =>
|
||||
@@ -283,7 +472,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get settings_rebootDevice => 'Apparaat opnieuw opstarten';
|
||||
|
||||
@override
|
||||
String get settings_rebootDeviceSubtitle => 'Herstart het MeshCore apparaat';
|
||||
String get settings_rebootDeviceSubtitle => 'Herstart het MeshCore-apparaat';
|
||||
|
||||
@override
|
||||
String get settings_rebootDeviceConfirm =>
|
||||
@@ -367,7 +556,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get settings_codingRate => 'Codeertarief';
|
||||
|
||||
@override
|
||||
String get settings_txPower => 'TX Vermogen (dBm)';
|
||||
String get settings_txPower => 'TX-Vermogen (dBm)';
|
||||
|
||||
@override
|
||||
String get settings_txPowerHelper => '0 - 22';
|
||||
@@ -376,11 +565,11 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get settings_txPowerInvalid => 'Ongeldige TX-vermogen (0-22 dBm)';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeat => 'Herhalen: Afgekoppeld';
|
||||
String get settings_clientRepeat => 'Off-Grid Herhalen';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.';
|
||||
'Laat dit apparaat de berichten van andere apparaten doorsturen.';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
@@ -544,6 +733,49 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get appSettings_autoRouteRotationDisabled =>
|
||||
'Automatische route rotatie is uitgeschakeld';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeight => 'Maximale gewicht voor de route';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeightSubtitle =>
|
||||
'Het maximale gewicht dat een route kan bereiken door succesvolle leveringen.';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeight => 'เริ่มต้น gewicht van de route';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeightSubtitle =>
|
||||
'Startgewicht voor nieuwe, ontdekte routes';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrement =>
|
||||
'Toename in het gewicht van het succes';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||
'Gewicht wordt toegevoegd aan een route na een succesvolle levering.';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrement =>
|
||||
'Vermindering van het gewicht van fouten';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||
'Gewicht verwijderd van een pad na een mislukte levering';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetries =>
|
||||
'Aantal pogingen om berichten te versturen';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Aantal pogingen om een bericht opnieuw te versturen voordat het als mislukt wordt gemarkeerd';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Batterij';
|
||||
|
||||
@@ -614,19 +846,19 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get appSettings_allTime => 'Altijd';
|
||||
|
||||
@override
|
||||
String get appSettings_lastHour => 'Laat uur';
|
||||
String get appSettings_lastHour => 'Afgelopen uur';
|
||||
|
||||
@override
|
||||
String get appSettings_last6Hours => 'laatste 6 uur';
|
||||
String get appSettings_last6Hours => 'Afgelopen 6 uur';
|
||||
|
||||
@override
|
||||
String get appSettings_last24Hours => 'De laatste 24 uur';
|
||||
String get appSettings_last24Hours => 'Afgelopen 24 uur';
|
||||
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Laatste week';
|
||||
String get appSettings_lastWeek => 'Afgelopen week';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Offline Kaarten Cache';
|
||||
String get appSettings_offlineMapCache => 'Offline Kaartcache';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsTitle => 'Eenheden';
|
||||
@@ -750,6 +982,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'De groepnaam is verplicht.';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => 'Deze groepsnaam is gereserveerd';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'De groep \"$name\" bestaat al.';
|
||||
@@ -789,6 +1024,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';
|
||||
|
||||
@@ -916,32 +1185,32 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get channels_sortUnread => 'Ongelezen';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannel => 'Maak een Privé Kanaal';
|
||||
String get channels_createPrivateChannel => 'PrivéKanaal Aanmaken';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannelDesc =>
|
||||
'Beveiligd met een geheime sleutel.';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannel => 'Sluit een Privé Kanaal aan';
|
||||
String get channels_joinPrivateChannel => 'PrivéKanaal Toetreden';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannelDesc =>
|
||||
'Handmatig een geheime sleutel invoeren.';
|
||||
'Voer handmatig een geheime sleutel in.';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannel => 'Sluit het Open Kanaal';
|
||||
String get channels_joinPublicChannel => 'Publiek Kanaal Toetreden';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannelDesc =>
|
||||
'Iedereen kan dit kanaal aanmelden.';
|
||||
'Iedereen kan toetreden tot dit kanaal.';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannel => 'Sluit een Hashtag Kanaal';
|
||||
String get channels_joinHashtagChannel => 'Hashtag-kanaal Aanmaken';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannelDesc =>
|
||||
'Iedereen kan lid worden van hashtag-kanalen.';
|
||||
'Iedereen kan toetreden tot hashtag-kanalen.';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCode => 'Scan een QR-code';
|
||||
@@ -958,6 +1227,14 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'Nog geen berichten.';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Verzend bericht';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Verstuur een bericht naar $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => 'Een bericht sturen om te beginnen';
|
||||
|
||||
@@ -977,11 +1254,6 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Locatie';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Verstuur een bericht naar $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Type een bericht...';
|
||||
|
||||
@@ -1357,6 +1629,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Deel marker hier';
|
||||
|
||||
@override
|
||||
String get map_setAsMyLocation => 'Stel dit in als mijn locatie';
|
||||
|
||||
@override
|
||||
String get map_pinLabel => 'Label vastzetten';
|
||||
|
||||
@@ -1402,6 +1677,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';
|
||||
|
||||
@@ -1417,6 +1695,16 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get map_showSharedMarkers => 'Toon gedeelde markeringen';
|
||||
|
||||
@override
|
||||
String get map_showGuessedLocations =>
|
||||
'Toon de voorspelde locaties van de knopen';
|
||||
|
||||
@override
|
||||
String get map_showDiscoveryContacts => 'Ontdek contacten weergeven';
|
||||
|
||||
@override
|
||||
String get map_guessedLocation => 'Geroerde locatie';
|
||||
|
||||
@override
|
||||
String get map_lastSeenTime => 'Laatste Bekeken Tijd';
|
||||
|
||||
@@ -1424,7 +1712,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get map_sharedPin => 'Gedeelde pin';
|
||||
|
||||
@override
|
||||
String get map_joinRoom => 'Sluit Kamer';
|
||||
String get map_joinRoom => 'Kamer Toetreden';
|
||||
|
||||
@override
|
||||
String get map_manageRepeater => 'Beheer Repeater';
|
||||
@@ -1436,6 +1724,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';
|
||||
|
||||
@@ -1713,7 +2004,16 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get room_management => 'Beheer Server Kamer';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Beheerinstrumenten';
|
||||
String get repeater_guest => 'Informatie over herhalingsapparatuur';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Informatie over de server';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Beheerfuncties';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Gastenfuncties';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Status';
|
||||
@@ -1739,7 +2039,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get repeater_neighbors => 'Buren';
|
||||
|
||||
@override
|
||||
String get repeater_neighborsSubtitle => 'Bekijk nul hops buren.';
|
||||
String get repeater_neighborsSubtitle => 'Bekijk nul-hopsburen.';
|
||||
|
||||
@override
|
||||
String get repeater_settings => 'Instellingen';
|
||||
@@ -1747,6 +2047,14 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_settingsSubtitle => 'Configureer repeaterparameters';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin =>
|
||||
'Na het inloggen, klok synchroniseren';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Automatisch een \"klok synchroniseren\" bericht versturen na een succesvolle inlog.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'Status repeater';
|
||||
|
||||
@@ -1805,10 +2113,10 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get repeater_noiseFloor => 'Ruisvloer';
|
||||
|
||||
@override
|
||||
String get repeater_txAirtime => 'TX Airtime';
|
||||
String get repeater_txAirtime => 'TX-zendtijd';
|
||||
|
||||
@override
|
||||
String get repeater_rxAirtime => 'RX Airtime';
|
||||
String get repeater_rxAirtime => 'RX-zendtijd';
|
||||
|
||||
@override
|
||||
String get repeater_packetStatistics => 'Pakketstatistieken';
|
||||
@@ -1853,7 +2161,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_settingsTitle => 'Repeater Instellingen';
|
||||
String get repeater_settingsTitle => 'Repeaterinstellingen';
|
||||
|
||||
@override
|
||||
String get repeater_basicSettings => 'Basisinstellingen';
|
||||
@@ -1862,19 +2170,19 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get repeater_repeaterName => 'Repeaternaam';
|
||||
|
||||
@override
|
||||
String get repeater_repeaterNameHelper => 'Weergave naam voor deze repeater';
|
||||
String get repeater_repeaterNameHelper => 'Weergavenaam voor deze repeater';
|
||||
|
||||
@override
|
||||
String get repeater_adminPassword => 'Admin wachtwoord';
|
||||
|
||||
@override
|
||||
String get repeater_adminPasswordHelper => 'Volledige toegangspaswoord';
|
||||
String get repeater_adminPasswordHelper => 'Wachtwoord administratortoegang';
|
||||
|
||||
@override
|
||||
String get repeater_guestPassword => 'Wachtwoord Gast';
|
||||
String get repeater_guestPassword => 'Gast wachtwoord';
|
||||
|
||||
@override
|
||||
String get repeater_guestPasswordHelper => 'Leesbeheer wachtwoord';
|
||||
String get repeater_guestPasswordHelper => 'Wachtwoord gasttoegen';
|
||||
|
||||
@override
|
||||
String get repeater_radioSettings => 'Radio Instellingen';
|
||||
@@ -1901,7 +2209,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get repeater_codingRate => 'Codeertarief';
|
||||
|
||||
@override
|
||||
String get repeater_locationSettings => 'Locatie Instellingen';
|
||||
String get repeater_locationSettings => 'Locatie-instellingen';
|
||||
|
||||
@override
|
||||
String get repeater_latitude => 'Breedtegraad';
|
||||
@@ -1933,14 +2241,14 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
'Toegestane leesbeheer toegang voor gasten.';
|
||||
|
||||
@override
|
||||
String get repeater_privacyMode => 'Privacy Modus';
|
||||
String get repeater_privacyMode => 'Privacymodus';
|
||||
|
||||
@override
|
||||
String get repeater_privacyModeSubtitle =>
|
||||
'Naam/locatie verbergen in advertenties';
|
||||
|
||||
@override
|
||||
String get repeater_advertisementSettings => 'Advertentie Instellingen';
|
||||
String get repeater_advertisementSettings => 'Advertentie-instellingen';
|
||||
|
||||
@override
|
||||
String get repeater_localAdvertInterval => 'Lokale Advertentie Interval';
|
||||
@@ -2045,7 +2353,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get repeater_refreshGuestAccess => 'Toegang Gast Vernieuwen';
|
||||
|
||||
@override
|
||||
String get repeater_refreshPrivacyMode => 'Privacy Mode vernieuwen';
|
||||
String get repeater_refreshPrivacyMode => 'Privacymode vernieuwen';
|
||||
|
||||
@override
|
||||
String get repeater_refreshAdvertisementSettings =>
|
||||
@@ -2071,10 +2379,10 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get repeater_commandHelp => 'Help';
|
||||
|
||||
@override
|
||||
String get repeater_clearHistory => 'Verwijder Geschiedenis';
|
||||
String get repeater_clearHistory => 'Geschiedenis Verwijderen';
|
||||
|
||||
@override
|
||||
String get repeater_noCommandsSent => 'Geen commando\'s verzonden nog.';
|
||||
String get repeater_noCommandsSent => 'Nog geen commando\'s verzonden.';
|
||||
|
||||
@override
|
||||
String get repeater_typeCommandOrUseQuick =>
|
||||
@@ -2101,28 +2409,34 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickGetName => 'Haal Naam op';
|
||||
String get repeater_cliQuickGetName => 'Naam opvragen';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickGetRadio => 'Radio ontvangen';
|
||||
String get repeater_cliQuickGetRadio => 'Radio-instellingen opvragen';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickGetTx => 'Krijg TX';
|
||||
String get repeater_cliQuickGetTx => 'TX opvragen';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickNeighbors => 'Buren';
|
||||
String get repeater_cliQuickNeighbors => 'Buren opvragen';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickVersion => 'Versie';
|
||||
String get repeater_cliQuickVersion => 'Versie opvragen';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickAdvertise => 'Advertenties';
|
||||
String get repeater_cliQuickAdvertise => 'Advertenties opvragen';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Tijd';
|
||||
String get repeater_cliQuickClock => 'Tijd opvragen';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Verstuurt een advertentiepakket';
|
||||
String get repeater_cliQuickClockSync => 'Kloksynchronisatie';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Ontdek Buren';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Advertentie uitzenden';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpReboot =>
|
||||
@@ -2394,7 +2708,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get telemetry_voltageLabel => 'Spanning';
|
||||
|
||||
@override
|
||||
String get telemetry_mcuTemperatureLabel => 'MCU Temperatuur';
|
||||
String get telemetry_mcuTemperatureLabel => 'MCU-temperatuur';
|
||||
|
||||
@override
|
||||
String get telemetry_temperatureLabel => 'Temperatuur';
|
||||
@@ -2435,7 +2749,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbors => 'Herhalingen Buren';
|
||||
String get neighbors_repeatersNeighbors => 'Repeatbburen';
|
||||
|
||||
@override
|
||||
String get neighbors_noData => 'Geen gegevens van buren beschikbaar.';
|
||||
@@ -2734,7 +3048,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get listFilter_latestMessages => 'Recente berichten';
|
||||
|
||||
@override
|
||||
String get listFilter_heardRecently => 'Hoor je onlangs';
|
||||
String get listFilter_heardRecently => 'Recent gezien';
|
||||
|
||||
@override
|
||||
String get listFilter_az => 'A-Z';
|
||||
@@ -2983,7 +3297,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
'Contact uit klembord toevoegen';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContact => 'Kontakt naar Klembord kopiëren';
|
||||
String get contacts_ShareContact => 'Contact naar Klembord kopiëren';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContactZeroHop => 'Contact delen via advertentie';
|
||||
@@ -3106,4 +3420,273 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Laatst gezien';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Instellingen voor contacten';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Automatische detectie';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Andere instellingen voor contactgerelateerde zaken';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Gebruikers automatisch toevoegen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Sta toe dat de companion automatisch ontdekte gebruikers toevoegt';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Automatisch herhalingstoestellen toevoegen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Sta toe dat de companion automatisch ontdekte repeaters toevoegt';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Automatisch kamerservers toevoegen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Sta toe dat de companion automatisch ontdekte kamer servers toevoegt.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Automatisch sensoren toevoegen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Sta toe dat de companion automatisch ontdekte sensoren toevoegt';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle => 'Overschrijf Oudste';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'Wanneer de contactenlijst vol is, wordt de oudste niet-favoriete contactpersoon vervangen.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Ontdekte contacten';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => 'Geen overeenkomende contacten';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Ontdekte contacten zoeken';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Contact toegevoegd';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Contact toevoegen';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact => 'Kopieer contact naar klembord';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Contact verwijderen';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Verwijder alle ontdekte contacten';
|
||||
|
||||
@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';
|
||||
|
||||
@override
|
||||
String get translation_title => 'Vertaling';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Activeer vertaling';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Vertaal inkomende berichten en maak het mogelijk om berichten vooraf te vertalen.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Vertaal voor verzending';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Stelt de standaardstatus van het pictogram voor de vertaling van de componist in.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Doeltaal';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => 'Gebruik de taal van de app';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Gedownloade model';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel =>
|
||||
'Voorgeprogrammeerd Hugging Face-model';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => 'URL van de handleiding';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Download het model';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Downloaden...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Werken...';
|
||||
|
||||
@override
|
||||
String get translation_stop => 'Stoppen';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Het samenvoegen van de gedownloade stukken tot één eindbestand...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Gedownloade modellen';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Model verwijderen';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded => 'Vertalingmodel gedownload.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'Download is afgebroken.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'Download mislukt: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst =>
|
||||
'Voer eerst een URL van een model in.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Toon PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingHidePin => 'PIN verbergen';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingPinTitle => 'Bluetooth‑koppelings‑PIN';
|
||||
|
||||
@override
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Voer PIN in voor $deviceName (laat leeg als er geen is).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Berichtvertaling';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending => 'Vertaal voor verzending';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'De berichten worden vertaald voordat ze verzonden worden.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Stuur berichten in de oorspronkelijke, getypte taal.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Vertalen naar $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Opties voor vertaling';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Taal van het systeem';
|
||||
}
|
||||
|
||||
+827
-226
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get common_delete => 'Excluir';
|
||||
|
||||
@override
|
||||
String get common_deleteAll => 'Excluir Tudo';
|
||||
|
||||
@override
|
||||
String get common_close => 'Fechar';
|
||||
|
||||
@@ -108,6 +111,136 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTcpLabel => 'TCP';
|
||||
|
||||
@override
|
||||
String get tcpScreenTitle => 'Estabelecer conexão via TCP';
|
||||
|
||||
@override
|
||||
String get tcpHostLabel => 'Endereço IP';
|
||||
|
||||
@override
|
||||
String get tcpHostHint => '192.168.40.10';
|
||||
|
||||
@override
|
||||
String get tcpPortLabel => 'Porta';
|
||||
|
||||
@override
|
||||
String get tcpPortHint => '5000';
|
||||
|
||||
@override
|
||||
String get tcpStatus_notConnected => 'Insira o endereço final e conecte-se.';
|
||||
|
||||
@override
|
||||
String tcpStatus_connectingTo(String endpoint) {
|
||||
return 'Conectando a $endpoint...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tcpErrorHostRequired => 'É necessário fornecer um endereço IP.';
|
||||
|
||||
@override
|
||||
String get tcpErrorPortInvalid =>
|
||||
'O valor do porto deve estar entre 1 e 65535.';
|
||||
|
||||
@override
|
||||
String get tcpErrorUnsupported =>
|
||||
'O protocolo TCP não é suportado nesta plataforma.';
|
||||
|
||||
@override
|
||||
String get tcpErrorTimedOut => 'A conexão TCP expirou.';
|
||||
|
||||
@override
|
||||
String tcpConnectionFailed(String error) {
|
||||
return 'Falha na conexão TCP: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Conecte via USB';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Selecione um dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Selecione um dispositivo USB';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'A comunicação serial via USB está ativa em dispositivos Android e plataformas de desktop compatíveis.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Nenhum dispositivo USB encontrado. Conecte um e atualize.';
|
||||
|
||||
@override
|
||||
String get usbErrorPermissionDenied =>
|
||||
'A permissão para acesso via USB foi negada.';
|
||||
|
||||
@override
|
||||
String get usbErrorDeviceMissing =>
|
||||
'O dispositivo USB selecionado não está mais disponível.';
|
||||
|
||||
@override
|
||||
String get usbErrorInvalidPort => 'Selecione um dispositivo USB válido.';
|
||||
|
||||
@override
|
||||
String get usbErrorBusy =>
|
||||
'Já existe uma solicitação de conexão USB em andamento.';
|
||||
|
||||
@override
|
||||
String get usbErrorNotConnected => 'Não há nenhum dispositivo USB conectado.';
|
||||
|
||||
@override
|
||||
String get usbErrorOpenFailed =>
|
||||
'Não foi possível abrir o dispositivo USB selecionado.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectFailed =>
|
||||
'Não foi possível conectar ao dispositivo USB selecionado.';
|
||||
|
||||
@override
|
||||
String get usbErrorUnsupported =>
|
||||
'A comunicação serial via USB não é suportada nesta plataforma.';
|
||||
|
||||
@override
|
||||
String get usbErrorAlreadyActive => 'A conexão USB já está ativa.';
|
||||
|
||||
@override
|
||||
String get usbErrorNoDeviceSelected =>
|
||||
'Nenhum dispositivo USB foi selecionado.';
|
||||
|
||||
@override
|
||||
String get usbErrorPortClosed => 'A conexão USB não está ativa.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectTimedOut =>
|
||||
'A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion.';
|
||||
|
||||
@override
|
||||
String get usbFallbackDeviceName => 'Dispositivo de Serial para a Web';
|
||||
|
||||
@override
|
||||
String get usbStatus_notConnected => 'Selecione um dispositivo USB';
|
||||
|
||||
@override
|
||||
String get usbStatus_connecting => 'Conectando ao dispositivo USB...';
|
||||
|
||||
@override
|
||||
String get usbStatus_searching => 'Procurando por dispositivos USB...';
|
||||
|
||||
@override
|
||||
String usbConnectionFailed(String error) {
|
||||
return 'Falha na conexão USB: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Procurando por dispositivos...';
|
||||
|
||||
@@ -150,6 +283,13 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Por favor, ative o Bluetooth para escanear por dispositivos.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Navegador Chrome necessário';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Esta aplicação web requer o Google Chrome ou um navegador baseado no Chromium para suporte de Bluetooth.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Ative o Bluetooth';
|
||||
|
||||
@@ -234,6 +374,13 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get settings_longitude => 'Longitude';
|
||||
|
||||
@override
|
||||
String get settings_contactSettings => 'Configurações de Contato';
|
||||
|
||||
@override
|
||||
String get settings_contactSettingsSubtitle =>
|
||||
'Configurações para como os contatos são adicionados';
|
||||
|
||||
@override
|
||||
String get settings_privacyMode => 'Modo de Privacidade';
|
||||
|
||||
@@ -251,6 +398,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';
|
||||
|
||||
@@ -549,6 +741,49 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get appSettings_autoRouteRotationDisabled =>
|
||||
'Rotação de roteamento automático desativada';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeight => 'Peso Máximo da Rota';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeightSubtitle =>
|
||||
'Peso máximo que um determinado percurso pode acumular com entregas bem-sucedidas.';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeight => 'Peso Inicial da Rota';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeightSubtitle =>
|
||||
'Peso inicial para novos caminhos descobertos';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrement =>
|
||||
'Aumento do peso para indicar sucesso';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||
'Peso adicionado a um caminho após a entrega bem-sucedida.';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrement =>
|
||||
'Redução do peso da falha';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||
'Peso removido de um caminho após uma tentativa de entrega malsucedida.';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetries =>
|
||||
'Número máximo de tentativas de envio de mensagens';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Número de tentativas de reenvio antes de classificar uma mensagem como falha.';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Bateria';
|
||||
|
||||
@@ -756,6 +991,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'O nome do grupo é obrigatório.';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => 'Este nome de grupo está reservado';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'O grupo \"$name\" já existe';
|
||||
@@ -795,6 +1033,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';
|
||||
|
||||
@@ -964,6 +1238,14 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'Ainda não existem mensagens.';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Enviar mensagem';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Enviar uma mensagem para $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => 'Enviar uma mensagem para começar';
|
||||
|
||||
@@ -983,11 +1265,6 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Localização';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Enviar uma mensagem para $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Digite uma mensagem...';
|
||||
|
||||
@@ -1364,6 +1641,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Compartilhar marcador aqui';
|
||||
|
||||
@override
|
||||
String get map_setAsMyLocation => 'Defina minha localização';
|
||||
|
||||
@override
|
||||
String get map_pinLabel => 'Rótulo de marcador';
|
||||
|
||||
@@ -1409,6 +1689,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';
|
||||
|
||||
@@ -1424,6 +1707,16 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get map_showSharedMarkers => 'Mostrar marcadores compartilhados';
|
||||
|
||||
@override
|
||||
String get map_showGuessedLocations =>
|
||||
'Mostrar as localizações dos nós estimados';
|
||||
|
||||
@override
|
||||
String get map_showDiscoveryContacts => 'Mostrar Contatos de Descoberta';
|
||||
|
||||
@override
|
||||
String get map_guessedLocation => 'Localização estimada';
|
||||
|
||||
@override
|
||||
String get map_lastSeenTime => 'Último Tempo de Visualização';
|
||||
|
||||
@@ -1442,6 +1735,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';
|
||||
|
||||
@@ -1719,9 +2015,18 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'Gerenciamento de Servidor de Sala';
|
||||
|
||||
@override
|
||||
String get repeater_guest => 'Informações sobre repetidores';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Informações do Servidor';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Ferramentas de Gerenciamento';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Ferramentas para hóspedes';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Status';
|
||||
|
||||
@@ -1754,6 +2059,14 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_settingsSubtitle => 'Configurar parâmetros do repetidor';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin =>
|
||||
'Sincronização do relógio após o login';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Enviar automaticamente a sincronização do \"relógio\" após um login bem-sucedido.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'Status do Repetidor';
|
||||
|
||||
@@ -2130,6 +2443,12 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Relógio';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'Sincronização do Relógio';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Descobrir Vizinhos';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Envia um pacote de anúncios';
|
||||
|
||||
@@ -3114,4 +3433,274 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Visto pela última vez';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Configurações de contatos';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Descoberta Automática';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Outras configurações relacionadas a contatos';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Adicionar usuários automaticamente';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Permitir que o companheiro adicione automaticamente os usuários descobertos.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Adicionar repetidores automaticamente';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Permitir que o companheiro adicione automaticamente os repetidores descobertos.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Adicionar automaticamente servidores de sala';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Permitir que o companheiro adicione automaticamente os servidores de salas descobertos.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Adicionar sensores automaticamente';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Permitir que o companheiro adicione automaticamente sensores descobertos.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle =>
|
||||
'Sobrescrever o Mais Antigo';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'Quando a lista de contatos estiver cheia, o contato mais antigo não favoritado será substituído.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Contatos Descobertos';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => 'Nenhum contato correspondente';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Pesquisar contatos descobertos';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Contato adicionado';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Adicionar Contato';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact =>
|
||||
'Copiar Contato para a área de transferência';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Excluir Contato';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Excluir Todos os Contatos Descobertos';
|
||||
|
||||
@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';
|
||||
|
||||
@override
|
||||
String get translation_title => 'Tradução';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Ativar a tradução';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Traduzir mensagens recebidas e permitir a tradução antes do envio.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Traduza antes de enviar';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Controla o estado padrão do ícone de tradução do compositor.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Língua-alvo';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => 'Utilize o idioma da aplicação';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Modelo baixado';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel =>
|
||||
'Modelo pré-definido da Hugging Face';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => 'URL do modelo manual';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Baixar modelo';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Baixando...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Trabalhando...';
|
||||
|
||||
@override
|
||||
String get translation_stop => 'Pare';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Combinando os fragmentos baixados em um único arquivo...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Modelos baixados';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Excluir modelo';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded => 'Modelo de tradução baixado.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'Download interrompido.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'Falha na descarga: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst => 'Insira primeiro a URL do modelo.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Mostrar PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingHidePin => 'Ocultar PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingPinTitle => 'PIN de emparelhamento Bluetooth';
|
||||
|
||||
@override
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Insira o PIN para $deviceName (deixe em branco se não houver).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Tradução da mensagem';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending => 'Traduzir antes de enviar';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'As mensagens serão traduzidas antes de serem enviadas.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Envie mensagens no idioma original, conforme digitado.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Traduzir para $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Opções de tradução';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Idioma do sistema';
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get common_delete => 'Удалить';
|
||||
|
||||
@override
|
||||
String get common_deleteAll => 'Удалить все';
|
||||
|
||||
@override
|
||||
String get common_close => 'Закрыть';
|
||||
|
||||
@@ -108,6 +111,137 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTcpLabel => 'TCP';
|
||||
|
||||
@override
|
||||
String get tcpScreenTitle => 'Установить соединение по протоколу TCP';
|
||||
|
||||
@override
|
||||
String get tcpHostLabel => 'IP-адрес';
|
||||
|
||||
@override
|
||||
String get tcpHostHint => '192.168.40.10';
|
||||
|
||||
@override
|
||||
String get tcpPortLabel => 'Порт';
|
||||
|
||||
@override
|
||||
String get tcpPortHint => '5000';
|
||||
|
||||
@override
|
||||
String get tcpStatus_notConnected => 'Введите адрес и подключитесь.';
|
||||
|
||||
@override
|
||||
String tcpStatus_connectingTo(String endpoint) {
|
||||
return 'Подключение к $endpoint...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tcpErrorHostRequired => 'Необходимо указать IP-адрес.';
|
||||
|
||||
@override
|
||||
String get tcpErrorPortInvalid =>
|
||||
'Порт должен находиться в диапазоне от 1 до 65535.';
|
||||
|
||||
@override
|
||||
String get tcpErrorUnsupported =>
|
||||
'Протокол TCP не поддерживается на этой платформе.';
|
||||
|
||||
@override
|
||||
String get tcpErrorTimedOut => 'Соединение TCP не удалось установить.';
|
||||
|
||||
@override
|
||||
String tcpConnectionFailed(String error) {
|
||||
return 'Не удалось установить соединение TCP: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Подключение через USB';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Выберите обнаруженное устройство с последовательным интерфейсом и подключите его напрямую к вашему узлу MeshCore.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Выберите USB-устройство';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'USB-серийный порт активен на поддерживаемых устройствах Android и на настольных платформах.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Не обнаружено устройств USB. Подключите одно из них и обновите список.';
|
||||
|
||||
@override
|
||||
String get usbErrorPermissionDenied =>
|
||||
'Запрос на доступ через USB был отклонен.';
|
||||
|
||||
@override
|
||||
String get usbErrorDeviceMissing =>
|
||||
'Выбранное USB-устройство больше недоступно.';
|
||||
|
||||
@override
|
||||
String get usbErrorInvalidPort => 'Выберите действительное USB-устройство.';
|
||||
|
||||
@override
|
||||
String get usbErrorBusy =>
|
||||
'Еще одно запрошенное соединение через USB уже находится в процессе.';
|
||||
|
||||
@override
|
||||
String get usbErrorNotConnected => 'Ни одно USB-устройство не подключено.';
|
||||
|
||||
@override
|
||||
String get usbErrorOpenFailed =>
|
||||
'Не удалось открыть выбранное USB-устройство.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectFailed =>
|
||||
'Не удалось установить соединение с выбранным USB-устройством.';
|
||||
|
||||
@override
|
||||
String get usbErrorUnsupported =>
|
||||
'Поддержка последовательного USB отсутствует на данной платформе.';
|
||||
|
||||
@override
|
||||
String get usbErrorAlreadyActive => 'USB-соединение уже установлено.';
|
||||
|
||||
@override
|
||||
String get usbErrorNoDeviceSelected =>
|
||||
'Не было выбрано ни одно устройство USB.';
|
||||
|
||||
@override
|
||||
String get usbErrorPortClosed => 'USB-соединение не установлено.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectTimedOut =>
|
||||
'Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion.';
|
||||
|
||||
@override
|
||||
String get usbFallbackDeviceName =>
|
||||
'Устройство для последовательного подключения к сети';
|
||||
|
||||
@override
|
||||
String get usbStatus_notConnected => 'Выберите USB-устройство';
|
||||
|
||||
@override
|
||||
String get usbStatus_connecting => 'Подключение к USB-устройству...';
|
||||
|
||||
@override
|
||||
String get usbStatus_searching => 'Поиск USB-устройств...';
|
||||
|
||||
@override
|
||||
String usbConnectionFailed(String error) {
|
||||
return 'Не удалось установить соединение через USB: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Поиск устройств...';
|
||||
|
||||
@@ -149,6 +283,13 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Пожалуйста, включите Bluetooth, чтобы найти устройства.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Требуется браузер Chrome';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Включите Bluetooth';
|
||||
|
||||
@@ -232,6 +373,13 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get settings_longitude => 'Долгота';
|
||||
|
||||
@override
|
||||
String get settings_contactSettings => 'Настройки контактов';
|
||||
|
||||
@override
|
||||
String get settings_contactSettingsSubtitle =>
|
||||
'Настройки добавления контактов';
|
||||
|
||||
@override
|
||||
String get settings_privacyMode => 'Режим конфиденциальности';
|
||||
|
||||
@@ -250,6 +398,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 => 'Действия';
|
||||
|
||||
@@ -548,6 +741,50 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get appSettings_autoRouteRotationDisabled =>
|
||||
'Автоматическое переключение маршрутов отключено';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeight =>
|
||||
'Максимальный допустимый вес маршрута';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeightSubtitle =>
|
||||
'Максимальный вес, который может быть перевезён по определённому маршруту при успешных доставках.';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeight => 'Начальный вес маршрута';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeightSubtitle =>
|
||||
'Начальный вес для новых, только что открытых маршрутов';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrement =>
|
||||
'Увеличение веса успеха';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||
'Вес, добавленный к маршруту после успешной доставки.';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrement =>
|
||||
'Уменьшение веса неудачи';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||
'Вес, который был удален с пути после неудачной доставки.';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetries =>
|
||||
'Максимальное количество повторных попыток отправки сообщения';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Батарея';
|
||||
|
||||
@@ -754,6 +991,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'Имя группы обязательно';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => 'Это имя группы зарезервировано';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'Группа \"$name\" уже существует';
|
||||
@@ -793,6 +1033,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 => 'Каналы';
|
||||
|
||||
@@ -962,6 +1238,14 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'Сообщений пока нет';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Отправить сообщение';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Отправить сообщение $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => 'Отправьте сообщение, чтобы начать';
|
||||
|
||||
@@ -981,11 +1265,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Местоположение';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Отправить сообщение $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Напишите сообщение...';
|
||||
|
||||
@@ -1365,6 +1644,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Поделиться меткой здесь';
|
||||
|
||||
@override
|
||||
String get map_setAsMyLocation => 'Установить мое местоположение';
|
||||
|
||||
@override
|
||||
String get map_pinLabel => 'Метка';
|
||||
|
||||
@@ -1410,6 +1692,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get map_otherNodes => 'Другие ноды';
|
||||
|
||||
@override
|
||||
String get map_showOverlaps => 'Перекрытия ключа повтора';
|
||||
|
||||
@override
|
||||
String get map_keyPrefix => 'Префикс ключа';
|
||||
|
||||
@@ -1425,6 +1710,16 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get map_showSharedMarkers => 'Показывать общие метки';
|
||||
|
||||
@override
|
||||
String get map_showGuessedLocations =>
|
||||
'Отобразить предполагаемые места расположения узлов';
|
||||
|
||||
@override
|
||||
String get map_showDiscoveryContacts => 'Показать контакты Discovery';
|
||||
|
||||
@override
|
||||
String get map_guessedLocation => 'Угаданное место';
|
||||
|
||||
@override
|
||||
String get map_lastSeenTime => 'Время последнего появления';
|
||||
|
||||
@@ -1443,6 +1738,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get map_runTrace => 'Запустить трассировку пути';
|
||||
|
||||
@override
|
||||
String get map_runTraceWithReturnPath => 'Вернуться обратно по тому же пути';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Удалить последний';
|
||||
|
||||
@@ -1721,9 +2019,18 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'Управление сервером комнат';
|
||||
|
||||
@override
|
||||
String get repeater_guest => 'Информация о ретрансляторе';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Информация о сервере';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Инструменты управления';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Инструменты для гостей';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Статус';
|
||||
|
||||
@@ -1756,6 +2063,14 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_settingsSubtitle => 'Настройка параметров репитера';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin =>
|
||||
'Синхронизация часов после входа в систему';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Автоматически отправлять сообщение \"синхронизация времени\" после успешной авторизации.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'Статус репитера';
|
||||
|
||||
@@ -2132,6 +2447,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Время';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'Синхронизация часов';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Обнаружить Соседей';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Отправляет пакет анонсирования';
|
||||
|
||||
@@ -3126,4 +3447,274 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Последний раз видели';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Настройки контактов';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Автоматическое обнаружение';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Другие настройки, связанные с контактами';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Автоматически добавлять пользователей';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Разрешить компаньону автоматически добавлять обнаруженных пользователей';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Автоматически добавлять ретрансляторы';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Разрешить спутнику автоматически добавлять обнаруженные ретрансляторы';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Автоматически добавлять серверы комнат';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Разрешить компаньону автоматически добавлять обнаруженные сервера комнат.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Автоматически добавлять датчики';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Разрешить компаньону автоматически добавлять обнаруженные датчики';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle =>
|
||||
'Перезаписать самое старое';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'Когда список контактов заполнен, будет заменен самый старый контакт, который не находится в избранном.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Обнаруженные контакты';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => 'Нет совпадающих контактов';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Найденные контакты поиска';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Контакт добавлен';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Добавить контакт';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact =>
|
||||
'Копировать контакт в буфер обмена';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Удалить контакт';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Удалить Все Обнаруженные Контакты';
|
||||
|
||||
@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 и время передачи';
|
||||
|
||||
@override
|
||||
String get translation_title => 'Перевод';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Включить перевод';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Переводить входящие сообщения и позволять предварительный перевод перед отправкой.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Переводить перед отправкой';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Управляет исходным состоянием значка перевода, предоставляемого редактором.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Целевой язык';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => 'Используйте язык приложения';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Загруженная модель';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel =>
|
||||
'Предопределенная модель от Hugging Face';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => 'Ссылка на руководство';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Скачать модель';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Загрузка...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Работа...';
|
||||
|
||||
@override
|
||||
String get translation_stop => 'Прекратите';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Объединение скачанных фрагментов в один финальный файл...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Загруженные модели';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Удалить модель';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded => 'Модель перевода загружена.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'Процесс загрузки был прерван.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'Не удалось скачать: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst => 'Сначала введите URL модели.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Показать PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingHidePin => 'Скрыть PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingPinTitle => 'PIN‑код сопряжения Bluetooth';
|
||||
|
||||
@override
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Введите PIN‑код для $deviceName (оставьте пустым, если нет).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Перевод сообщения';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending => 'Перевести перед отправкой';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'Сообщения будут переведены перед отправкой.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Отправляйте сообщения на языке, в котором они были изначально набраны.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Перевести на $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Варианты перевода';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Язык системы';
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get common_delete => 'Odstrániť';
|
||||
|
||||
@override
|
||||
String get common_deleteAll => 'Zmazať všetko';
|
||||
|
||||
@override
|
||||
String get common_close => 'Zavrieť';
|
||||
|
||||
@@ -108,6 +111,135 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTcpLabel => 'TCP';
|
||||
|
||||
@override
|
||||
String get tcpScreenTitle => 'Spojte sa pomocou protokolu TCP';
|
||||
|
||||
@override
|
||||
String get tcpHostLabel => 'IP adresa';
|
||||
|
||||
@override
|
||||
String get tcpHostHint => '192.168.40.10';
|
||||
|
||||
@override
|
||||
String get tcpPortLabel => 'Port';
|
||||
|
||||
@override
|
||||
String get tcpPortHint => '5000';
|
||||
|
||||
@override
|
||||
String get tcpStatus_notConnected => 'Zadajte cieľovú adresu a pripojte sa.';
|
||||
|
||||
@override
|
||||
String tcpStatus_connectingTo(String endpoint) {
|
||||
return 'Pripojenie k $endpoint...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tcpErrorHostRequired => 'Je potrebné zadať IP adresu.';
|
||||
|
||||
@override
|
||||
String get tcpErrorPortInvalid => 'Číslo portu musí byť medzi 1 a 65535.';
|
||||
|
||||
@override
|
||||
String get tcpErrorUnsupported =>
|
||||
'Prevoz prostredníctvom protokolu TCP nie je na tejto platforme podporovaný.';
|
||||
|
||||
@override
|
||||
String get tcpErrorTimedOut => 'Pripojenie TCP vypršalo.';
|
||||
|
||||
@override
|
||||
String tcpConnectionFailed(String error) {
|
||||
return 'Neúspešné vytvorenie TCP spojenia: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Pripojte cez USB';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vašej MeshCore uzlu.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Vyberte USB zariadenie';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte.';
|
||||
|
||||
@override
|
||||
String get usbErrorPermissionDenied =>
|
||||
'Žiadosť o prístup cez USB bola zamietnutá.';
|
||||
|
||||
@override
|
||||
String get usbErrorDeviceMissing =>
|
||||
'Vybrané USB zariadenie už nie je dostupné.';
|
||||
|
||||
@override
|
||||
String get usbErrorInvalidPort => 'Vyberte platné USB zariadenie.';
|
||||
|
||||
@override
|
||||
String get usbErrorBusy =>
|
||||
'Ďalšia požiadavka na pripojenie cez USB je aktuálne v procese.';
|
||||
|
||||
@override
|
||||
String get usbErrorNotConnected => 'Nie je pripojené žiadne USB zariadenie.';
|
||||
|
||||
@override
|
||||
String get usbErrorOpenFailed =>
|
||||
'Nepodarilo sa otvoriť vybrané USB zariadenie.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectFailed =>
|
||||
'Nepodarilo sa sa sa pripojiť k vybranému USB zariadeniu.';
|
||||
|
||||
@override
|
||||
String get usbErrorUnsupported =>
|
||||
'Podpora USB sériového rozhrania nie je na tejto platforme dostupná.';
|
||||
|
||||
@override
|
||||
String get usbErrorAlreadyActive => 'Pripojenie cez USB je už aktivované.';
|
||||
|
||||
@override
|
||||
String get usbErrorNoDeviceSelected =>
|
||||
'Nebolo vybrané žiadne USB zariadenie.';
|
||||
|
||||
@override
|
||||
String get usbErrorPortClosed => 'Pripojenie cez USB nie je aktivované.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectTimedOut =>
|
||||
'Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion.';
|
||||
|
||||
@override
|
||||
String get usbFallbackDeviceName => 'Webový sériový zariadenie';
|
||||
|
||||
@override
|
||||
String get usbStatus_notConnected => 'Vyberte USB zariadenie';
|
||||
|
||||
@override
|
||||
String get usbStatus_connecting => 'Pripojenie k USB zariadeniu...';
|
||||
|
||||
@override
|
||||
String get usbStatus_searching => 'Hľadanie USB zariadení...';
|
||||
|
||||
@override
|
||||
String usbConnectionFailed(String error) {
|
||||
return 'Neúspešné pripojenie cez USB: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Skrívania zariadení...';
|
||||
|
||||
@@ -150,6 +282,13 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Vyžaduje sa prehliadač Chrome';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Táto webová aplikácia vyžaduje Google Chrome alebo prehliadač založený na Chromium pre podporu Bluetooth.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Povolte Bluetooth';
|
||||
|
||||
@@ -233,6 +372,13 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get settings_longitude => 'Dĺžka';
|
||||
|
||||
@override
|
||||
String get settings_contactSettings => 'Nastavenia kontaktov';
|
||||
|
||||
@override
|
||||
String get settings_contactSettingsSubtitle =>
|
||||
'Nastavenia pre pridávanie kontaktov.';
|
||||
|
||||
@override
|
||||
String get settings_privacyMode => 'Režim ochrany súkromia';
|
||||
|
||||
@@ -249,6 +395,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';
|
||||
|
||||
@@ -541,6 +730,48 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
String get appSettings_autoRouteRotationDisabled =>
|
||||
'Automatické prekladanie trás pozastavené';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeight => 'Maximálna hmotnosť trasy';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeightSubtitle =>
|
||||
'Maximálna hmotnosť, ktorú môže trás prenášať vďaka úspešným zásielkam.';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeight => 'Počiatočná váha trasy';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeightSubtitle =>
|
||||
'Počiatočná váha pre nové, objavené cesty';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrement => 'Zvyšenie váhy úspechu';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||
'Hmotnosť pridaná k trase po úspešnej doručení';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrement =>
|
||||
'Sníženie váhy, ktorá sa používa na odhad rizika.';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||
'Hmotnosť odstránená z cesty po neúspešnej doručenie';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetries =>
|
||||
'Maximalný počet pokusov o doručenie správ';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Počet pokusov o odošleť pred označením správy ako neúspešnej';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Batéria';
|
||||
|
||||
@@ -748,6 +979,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'Skupina musí mať názov.';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => 'Tento názov skupiny je rezervovaný';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'Skupina \"$name\" už existuje';
|
||||
@@ -789,6 +1023,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';
|
||||
|
||||
@@ -957,6 +1226,14 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'Zatiaľ žiadne správy.';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Odoslať správu';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Pošli správu $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => 'Pošlite správu na začiatok';
|
||||
|
||||
@@ -976,11 +1253,6 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Lokalita';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Pošli správu $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Napište správu...';
|
||||
|
||||
@@ -1358,6 +1630,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Zdieľte značku tu';
|
||||
|
||||
@override
|
||||
String get map_setAsMyLocation => 'Nastavte ako moju polohu';
|
||||
|
||||
@override
|
||||
String get map_pinLabel => 'Označka upozornenia';
|
||||
|
||||
@@ -1403,6 +1678,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';
|
||||
|
||||
@@ -1418,6 +1696,16 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get map_showSharedMarkers => 'Zobraziť zdieľané značky';
|
||||
|
||||
@override
|
||||
String get map_showGuessedLocations =>
|
||||
'Zobraziť umiestnenia odhadnutých uzlov';
|
||||
|
||||
@override
|
||||
String get map_showDiscoveryContacts => 'Zobraziť kontakty objavov';
|
||||
|
||||
@override
|
||||
String get map_guessedLocation => 'Odhadnutá lokalita';
|
||||
|
||||
@override
|
||||
String get map_lastSeenTime => 'Posledný čas sledovania';
|
||||
|
||||
@@ -1436,6 +1724,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ý';
|
||||
|
||||
@@ -1713,9 +2004,18 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'Správa servera miestnosti';
|
||||
|
||||
@override
|
||||
String get repeater_guest => 'Informácie o opakovači';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Informácie o serveri';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Nástroje na správu';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Nástroje pre hostí';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Status';
|
||||
|
||||
@@ -1748,6 +2048,14 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_settingsSubtitle => 'Konfigurujte parametre opakovača';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin =>
|
||||
'Synchronizácia hodiniek po prihlávení';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Automaticky posielajte notifikáciu \"synchronizácia času\" po úspešnom prihládení.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'Status opakého zboru';
|
||||
|
||||
@@ -2118,6 +2426,12 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Hodiny';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'Synchronizácia hodin';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Objaviť susedov';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Odosiela reklamnú balíček.';
|
||||
|
||||
@@ -3101,4 +3415,272 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Naposledy videný';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Nastavenia kontaktov';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Automatické zisťovanie';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Ďalšie nastavenia súvisiace s kontaktami';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Automaticky pridávať užívateľov';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Povoliť spoločníkovi automaticky pridávať objavených užívateľov.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Automaticky pridávať opakovače';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Povoliť spoločníkovi automaticky pridávať objavené repeater.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Automaticky pridávať server miestnosti';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Povoliť spoločníkovi automaticky pridať objavené serverové miestnosti.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Automaticky pridávať senzory';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Povoliť spoločníkovi automaticky pridávať objavené senzory.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle => 'Prepísať najstaršie';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'Keď je zoznam kontaktov plný, bude nahradený najstarší neoznačený kontakt.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Objavené kontakty';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => 'Žiadne zhodné kontakty';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Vyhľadať objavené kontakty';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Kontakt bol pridaný';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Pridať kontakt';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact => 'Kopírovať kontakt do schránky';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Zmazať kontakt';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Zmazať všetky objavené kontakty';
|
||||
|
||||
@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';
|
||||
|
||||
@override
|
||||
String get translation_title => 'Preklad';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Aktivovať preklad';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Prekladajte prichádzajúce správy a umožnite ich preklad pred odoslaním.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Preložte pred odeslaním';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Riadi výchoce stav ikony pre preklad, ktorú používa program.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Cieľový jazyk';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => 'Použite jazyk aplikácie';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Stiahnutý model';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel =>
|
||||
'Prednastavený model od Hugging Face';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel =>
|
||||
'Odkaz na manuál (v elektronickej forme)';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Stiahnuť model';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Stiahnutie...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Práca...';
|
||||
|
||||
@override
|
||||
String get translation_stop => 'Zastavte';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Sliečenie stiahnutých častí do konečného súboru...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Stiahnuté modely';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Odstrániť model';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded => 'Model pre preklad bol stiahnutý.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'Stiahnutie bolo prerušené.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'Neúspešné stiahnutie: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst =>
|
||||
'Najprv zadajte URL pre konkrétny model.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Zobraziť PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingHidePin => 'Skryť PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingPinTitle => 'PIN pre párovanie cez Bluetooth';
|
||||
|
||||
@override
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Zadajte PIN pre $deviceName (ak neexistuje, nechajte prázdne).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Preklad textu';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending => 'Preložte pred odeslaním';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'Správy budú preložené, než budú odoslané.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Posielajte správy v pôvodnej písanom jazyku.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Preložte do $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Možnosti prekladania';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Jazyk systému';
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get common_delete => 'Izbrisati';
|
||||
|
||||
@override
|
||||
String get common_deleteAll => 'Izbriši vse';
|
||||
|
||||
@override
|
||||
String get common_close => 'Zapri';
|
||||
|
||||
@@ -108,6 +111,133 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTcpLabel => 'TCP';
|
||||
|
||||
@override
|
||||
String get tcpScreenTitle => 'Komunicirajte preko protokola TCP';
|
||||
|
||||
@override
|
||||
String get tcpHostLabel => 'IP naslov';
|
||||
|
||||
@override
|
||||
String get tcpHostHint => '192.168.40.10';
|
||||
|
||||
@override
|
||||
String get tcpPortLabel => 'Vrata';
|
||||
|
||||
@override
|
||||
String get tcpPortHint => '5000';
|
||||
|
||||
@override
|
||||
String get tcpStatus_notConnected => 'Vnesite končni naslov in se povežite';
|
||||
|
||||
@override
|
||||
String tcpStatus_connectingTo(String endpoint) {
|
||||
return 'Povezava z $endpoint...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tcpErrorHostRequired => 'Potrebna je IP-naslov.';
|
||||
|
||||
@override
|
||||
String get tcpErrorPortInvalid => 'Port mora biti med 1 in 65535.';
|
||||
|
||||
@override
|
||||
String get tcpErrorUnsupported =>
|
||||
'Transport preko protokola TCP ni podprt na tej platformi.';
|
||||
|
||||
@override
|
||||
String get tcpErrorTimedOut => 'Povezava TCP je presegla časovno obdobje.';
|
||||
|
||||
@override
|
||||
String tcpConnectionFailed(String error) {
|
||||
return 'Napaka pri povezavi TCP: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Povežite preko USB';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Izberite zaznano serijsko napravo in se neposredno povežite z vašo MeshCore napravo.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Izberite USB naprave';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'USB serijska povezava je aktivna na podprtih napravah Android in na desktop platformah.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Niti en USB naprave niso najdeni. Povežite eno in posodobite.';
|
||||
|
||||
@override
|
||||
String get usbErrorPermissionDenied =>
|
||||
'Dovoljenje za dostop preko USB-ja je bilo zavrnjeno.';
|
||||
|
||||
@override
|
||||
String get usbErrorDeviceMissing => 'Izbrani USB napravej je več ne.';
|
||||
|
||||
@override
|
||||
String get usbErrorInvalidPort => 'Izberite veljavno USB naprave.';
|
||||
|
||||
@override
|
||||
String get usbErrorBusy => 'Že je v teku zahteva za povezavo preko USB.';
|
||||
|
||||
@override
|
||||
String get usbErrorNotConnected => 'Ni priklopljenih USB naprave.';
|
||||
|
||||
@override
|
||||
String get usbErrorOpenFailed =>
|
||||
'Uspešno ni bilo mogo, da se odpre izbran naprave USB.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectFailed =>
|
||||
'Niso bilo mogoče uskladiti povezave z izbranim USB napom.';
|
||||
|
||||
@override
|
||||
String get usbErrorUnsupported =>
|
||||
'USB serijska komunikacija ni podprta na tej platformi.';
|
||||
|
||||
@override
|
||||
String get usbErrorAlreadyActive => 'USB povezava je že aktivirana.';
|
||||
|
||||
@override
|
||||
String get usbErrorNoDeviceSelected => 'Ni bilo izbranega USB naprave.';
|
||||
|
||||
@override
|
||||
String get usbErrorPortClosed => 'USB povezava ni aktivirana.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectTimedOut =>
|
||||
'Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion.';
|
||||
|
||||
@override
|
||||
String get usbFallbackDeviceName =>
|
||||
'Naprave za serijsko komunikacijo preko spleta';
|
||||
|
||||
@override
|
||||
String get usbStatus_notConnected => 'Izberite USB naprave.';
|
||||
|
||||
@override
|
||||
String get usbStatus_connecting => 'Povezava z USB napravo...';
|
||||
|
||||
@override
|
||||
String get usbStatus_searching => 'Iskanje USB naprav...';
|
||||
|
||||
@override
|
||||
String usbConnectionFailed(String error) {
|
||||
return 'Napaka pri povezavi preko USB: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Skeniram za naprave...';
|
||||
|
||||
@@ -150,6 +280,13 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Prosimo, vklopite Bluetooth, da lahko poiščete naprave.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Zahtevan brskalnik Chrome';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Ta spletna aplikacija za podporo Bluetooth zahteva Google Chrome ali brskalnik na osnovi Chromiuma.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Omogočite Bluetooth';
|
||||
|
||||
@@ -233,6 +370,13 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_longitude => 'Dolžina';
|
||||
|
||||
@override
|
||||
String get settings_contactSettings => 'Nastavitve stika';
|
||||
|
||||
@override
|
||||
String get settings_contactSettingsSubtitle =>
|
||||
'Nastavitve za dodajanje stikov.';
|
||||
|
||||
@override
|
||||
String get settings_privacyMode => 'Zasebnost';
|
||||
|
||||
@@ -249,6 +393,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';
|
||||
|
||||
@@ -543,6 +731,49 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String get appSettings_autoRouteRotationDisabled =>
|
||||
'Samodejno krmilno rotiranje je onemogočeno';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeight => 'Največja dovoljena teža poti';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeightSubtitle =>
|
||||
'Največja teža, ki jo lahko pot doseže s uspešnimi dostavnami.';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeight => 'Izvirna teža poti';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeightSubtitle =>
|
||||
'Izguba teže za nove, odkriti poti';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrement =>
|
||||
'Učinkovitost: povečanje';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||
'Težava, dodana poti po uspešni dostavi';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrement =>
|
||||
'Zmanjšanje teže, ki je povezana s pomanjkanjem';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||
'Težo, ki ni bila uspešno dostavljena, odstranili s poti.';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetries =>
|
||||
'Najve število poskusov pošiljanja sporočil';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Baterija';
|
||||
|
||||
@@ -748,6 +979,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'Ime skupine je obvezno.';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => 'To ime skupine je rezervirano';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'Skupina \"$name\" že obstaja';
|
||||
@@ -787,6 +1021,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';
|
||||
|
||||
@@ -955,6 +1224,14 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'Še ni sporočil.';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Pošlji sporočilo';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Pošlji sporočilo $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => 'Pošlji sporočilo za začetek.';
|
||||
|
||||
@@ -975,11 +1252,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Lokacija';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Pošlji sporočilo $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Vnesi sporočilo...';
|
||||
|
||||
@@ -1354,6 +1626,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Delite točke tukaj.';
|
||||
|
||||
@override
|
||||
String get map_setAsMyLocation => 'Nastavite to kot mojo lokacijo';
|
||||
|
||||
@override
|
||||
String get map_pinLabel => 'Oznaka za pritrditev';
|
||||
|
||||
@@ -1399,6 +1674,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';
|
||||
|
||||
@@ -1414,6 +1692,15 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get map_showSharedMarkers => 'Pokaži skupno označenja';
|
||||
|
||||
@override
|
||||
String get map_showGuessedLocations => 'Pokaži lokacije domnevnih not.';
|
||||
|
||||
@override
|
||||
String get map_showDiscoveryContacts => 'Prikaži odkritja kontaktov';
|
||||
|
||||
@override
|
||||
String get map_guessedLocation => 'Predpostavljena lokacija';
|
||||
|
||||
@override
|
||||
String get map_lastSeenTime => 'Datum zadnjega vpogleda';
|
||||
|
||||
@@ -1432,6 +1719,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';
|
||||
|
||||
@@ -1711,9 +2001,18 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'Upravljanje stremlišča';
|
||||
|
||||
@override
|
||||
String get repeater_guest => 'Informacije o ponovljalniku';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Informacije o strežniku';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Upravne orodje';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Naložila za goste';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Status';
|
||||
|
||||
@@ -1748,6 +2047,13 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String get repeater_settingsSubtitle =>
|
||||
'Konfigurirajte parametre ponovitelja';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin => 'Sinhronizacija ure po prijavi';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Samodejno po uspešnem vstopu pošljite obvestilo o sinhronizaciji časa.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'Status ponovitelja';
|
||||
|
||||
@@ -2122,6 +2428,12 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Ura';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'Usklajevanje ure';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Odkrijte sosede';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Pošlje paket oglasov';
|
||||
|
||||
@@ -3106,4 +3418,273 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Zadnjič videno';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Nastavitve stikov';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Avtomatsko odkrivanje';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle => 'Druge nastavitve v zvezi s stiki';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Avtomatsko dodaj uporabnike';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Dovoli spremljevalcu, da samodejno doda odkrite uporabnike.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Avtomatsko dodaj ponovitelje';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Dovoli spremljevalcu, da samodejno doda odkrite ponovitelje.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Avtomatsko dodaj strežnike sob';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Dovoli spremljevalcu, da samodejno doda odkrite strežnike sob.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Avtomatsko dodaj senzorje';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Dovoli spremljevalcu, da samodejno doda odkrite senzorje.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle => 'Prepiši najstarejše';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'Ko je seznam stikov poln, bo najstarejši nestarševski stik zamenjan.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Odkriti stiki';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => 'Ni ujemajočih stikov';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Najdeni stiki po iskanju';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Kontakt dodan';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Dodaj stik';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact => 'Kopiraj stik v odložišče';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Izbriši stik';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Izbriši vse odkrite kontakte';
|
||||
|
||||
@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';
|
||||
|
||||
@override
|
||||
String get translation_title => 'Prevod';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Omogočite prevod';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Prevedite vstopne sporočila in omogočite predhodno prevajanje.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Preprištejte, preden pošljete';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Ureja privzeto stanje ikone za prevod, ki jo uporablja avtor.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Ciljna jezika';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => 'Uporabite jezik aplikacije';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Naložen model';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel =>
|
||||
'Prednastavljeni model Hugging Face';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => 'URL za ročni model';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Prenesite model';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Izvajanje...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Delo...';
|
||||
|
||||
@override
|
||||
String get translation_stop => 'Prekliji';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Sklapljanje prenesenih delov v končni datoteko...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Naloženi modeli';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Izbrisati model';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded =>
|
||||
'Model za prevajanje je bil naložen.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'Prenos je bil prekinjen.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'Izgovoritev ni bila uspešna: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst => 'Najprej vnesite URL model.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Prikaži PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingHidePin => 'Skrij PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingPinTitle => 'Bluetooth PIN za seznanjanje';
|
||||
|
||||
@override
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Vnesite PIN za $deviceName (pustite prazno, če ga ni).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Prevod sporočila';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending =>
|
||||
'Preprištejte, preden pošljete';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'Vsebina sporočil bo prevedena, preden jih pošljemo.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Pošljite sporočila v originalnem tipkanem jeziku.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Prevesti v $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Možnosti prevoda';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Jezik sistema';
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get common_delete => 'Radera';
|
||||
|
||||
@override
|
||||
String get common_deleteAll => 'Ta bort alla';
|
||||
|
||||
@override
|
||||
String get common_close => 'Stänga';
|
||||
|
||||
@@ -108,6 +111,133 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTcpLabel => 'TCP';
|
||||
|
||||
@override
|
||||
String get tcpScreenTitle => 'Anslut via TCP';
|
||||
|
||||
@override
|
||||
String get tcpHostLabel => 'IP-adress';
|
||||
|
||||
@override
|
||||
String get tcpHostHint => '192.168.40.10';
|
||||
|
||||
@override
|
||||
String get tcpPortLabel => 'Port';
|
||||
|
||||
@override
|
||||
String get tcpPortHint => '5000';
|
||||
|
||||
@override
|
||||
String get tcpStatus_notConnected => 'Ange slutpunkt och anslut';
|
||||
|
||||
@override
|
||||
String tcpStatus_connectingTo(String endpoint) {
|
||||
return 'Anslutning till $endpoint...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tcpErrorHostRequired => 'IP-adress krävs.';
|
||||
|
||||
@override
|
||||
String get tcpErrorPortInvalid => 'Porten måste vara mellan 1 och 65535.';
|
||||
|
||||
@override
|
||||
String get tcpErrorUnsupported =>
|
||||
'TCP-transport fungerar inte på denna plattform.';
|
||||
|
||||
@override
|
||||
String get tcpErrorTimedOut => 'TCP-anslutningen har tidsut gått.';
|
||||
|
||||
@override
|
||||
String tcpConnectionFailed(String error) {
|
||||
return 'Fel vid TCP-anslutning: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Anslut via USB';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Välj en USB-enhet';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'USB-seriell kommunikation är aktiv på stödda Android-enheter och på skrivbordsplattformar.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Inga USB-enheter hittades. Anslut en och uppdatera.';
|
||||
|
||||
@override
|
||||
String get usbErrorPermissionDenied => 'Tillgången via USB nekas.';
|
||||
|
||||
@override
|
||||
String get usbErrorDeviceMissing =>
|
||||
'Den valda USB-enheten är inte längre tillgänglig.';
|
||||
|
||||
@override
|
||||
String get usbErrorInvalidPort => 'Välj en giltig USB-enhet.';
|
||||
|
||||
@override
|
||||
String get usbErrorBusy =>
|
||||
'En annan förfrågan om USB-anslutning är redan pågående.';
|
||||
|
||||
@override
|
||||
String get usbErrorNotConnected => 'Ingen USB-enhet är ansluten.';
|
||||
|
||||
@override
|
||||
String get usbErrorOpenFailed =>
|
||||
'Misslyckades med att öppna det valda USB-enheten.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectFailed =>
|
||||
'Kunde inte ansluta till det valda USB-enheten.';
|
||||
|
||||
@override
|
||||
String get usbErrorUnsupported =>
|
||||
'USB-seriell kommunikation stöds inte på denna plattform.';
|
||||
|
||||
@override
|
||||
String get usbErrorAlreadyActive => 'En USB-anslutning är redan aktiv.';
|
||||
|
||||
@override
|
||||
String get usbErrorNoDeviceSelected => 'Ingen USB-enhet valdes.';
|
||||
|
||||
@override
|
||||
String get usbErrorPortClosed => 'USB-anslutningen är inte aktiv.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectTimedOut =>
|
||||
'Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware.';
|
||||
|
||||
@override
|
||||
String get usbFallbackDeviceName => 'Web-serieenhet';
|
||||
|
||||
@override
|
||||
String get usbStatus_notConnected => 'Välj en USB-enhet';
|
||||
|
||||
@override
|
||||
String get usbStatus_connecting => 'Anslutning till USB-enhet...';
|
||||
|
||||
@override
|
||||
String get usbStatus_searching => 'Söker efter USB-enheter...';
|
||||
|
||||
@override
|
||||
String usbConnectionFailed(String error) {
|
||||
return 'Fel vid USB-anslutning: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Söker efter enheter...';
|
||||
|
||||
@@ -149,6 +279,13 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Vänligen aktivera Bluetooth för att söka efter enheter.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Chrome-webbläsare krävs';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Denna webbapplikation kräver Google Chrome oder en Chromium-baserader webbläsare för Bluetooth-stöd.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Aktivera Bluetooth';
|
||||
|
||||
@@ -232,6 +369,13 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get settings_longitude => 'Längdgrad';
|
||||
|
||||
@override
|
||||
String get settings_contactSettings => 'Kontaktinställningar';
|
||||
|
||||
@override
|
||||
String get settings_contactSettingsSubtitle =>
|
||||
'Inställningar för hur kontakter läggs till.';
|
||||
|
||||
@override
|
||||
String get settings_privacyMode => 'Privatläge';
|
||||
|
||||
@@ -248,6 +392,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';
|
||||
|
||||
@@ -538,6 +725,48 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
String get appSettings_autoRouteRotationDisabled =>
|
||||
'Automatisk ruttrotation är avstängd';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeight => 'Maximalt tillåtet vikt för rutten';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeightSubtitle =>
|
||||
'Maximal vikt som en leveransväg kan ackumulera från framgångsrika leveranser.';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeight => 'Initial vikt för rutt';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeightSubtitle =>
|
||||
'Initial vikt för nyligen upptäckta vägar';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrement =>
|
||||
'Ökning av vikt för framgång';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||
'Vikt läggs till en väg efter en lyckad leverans.';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrement =>
|
||||
'Minskning av vikten för misslyckande';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||
'Vikt som tagits bort från en väg efter ett misslyckat leveransförsök';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetries => 'Maximalt antal försök';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Antal försök att skicka om ett meddelande innan det markeras som misslyckat.';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Batteri';
|
||||
|
||||
@@ -744,6 +973,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'Gruppnamnet är obligatoriskt';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => 'Detta gruppnamn är reserverat';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'Gruppen \"$name\" finns redan.';
|
||||
@@ -783,6 +1015,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';
|
||||
|
||||
@@ -951,6 +1217,14 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'Inga meddelanden ännu';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Skicka meddelande';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Skicka ett meddelande till $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart =>
|
||||
'Skicka ett meddelande för att komma igång';
|
||||
@@ -972,11 +1246,6 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Plats';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Skicka ett meddelande till $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Skriv ett meddelande...';
|
||||
|
||||
@@ -1350,6 +1619,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Dela markeringen här';
|
||||
|
||||
@override
|
||||
String get map_setAsMyLocation => 'Ange som min plats';
|
||||
|
||||
@override
|
||||
String get map_pinLabel => 'Fästetikett';
|
||||
|
||||
@@ -1395,6 +1667,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';
|
||||
|
||||
@@ -1410,6 +1685,16 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get map_showSharedMarkers => 'Visa delade markörer';
|
||||
|
||||
@override
|
||||
String get map_showGuessedLocations =>
|
||||
'Visa upp de antagna nodernas placeringar';
|
||||
|
||||
@override
|
||||
String get map_showDiscoveryContacts => 'Visa Discovery-kontakter';
|
||||
|
||||
@override
|
||||
String get map_guessedLocation => 'Gissad plats';
|
||||
|
||||
@override
|
||||
String get map_lastSeenTime => 'Senaste Visats Tid';
|
||||
|
||||
@@ -1428,6 +1713,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';
|
||||
|
||||
@@ -1702,9 +1990,18 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'Rumserverhantering';
|
||||
|
||||
@override
|
||||
String get repeater_guest => 'Information om repetorer';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Information om servern';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Administrationsverktyg';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Gästverktyg';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Status';
|
||||
|
||||
@@ -1737,6 +2034,14 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_settingsSubtitle => 'Konfigurera återspolarparametrar';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin =>
|
||||
'Synkronisera klockan efter inloggning';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Automatiskt skicka \"klocksynkronisering\" efter en lyckad inloggning.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'Återspelsstatus';
|
||||
|
||||
@@ -2109,6 +2414,12 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Klocka';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'Synkronisera klocka';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Upptäck grannar';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Skickar ett annonspaket';
|
||||
|
||||
@@ -3084,4 +3395,274 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Senast sedd';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Kontaktinställningar';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Automatisk upptäckt';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Andra inställningar relaterade till kontakt';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Lägg till användare automatiskt';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Tillåt kompanjonen att automatiskt lägga till upptäckta användare';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Lägg till upprepande enheter automatiskt';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Tillåt kompanjonen att automatiskt lägga till upptäckta repeater.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Lägg automatiskt till rumsservrar';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Tillåt kompanjonen att automatiskt lägga till upptäckta rumsservrar.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Lägg till sensorer automatiskt';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Tillåt kompanjonen att automatiskt lägga till upptäckta sensorer.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle => 'Skriv över äldst';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'När kontaktlistan är full ersätts den äldsta icke-favoriterade kontakten.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Upptäckta kontakter';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => 'Inga matchande kontakter';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Sök uppfunna kontakter';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Kontakt tillagd';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Lägg till kontakt';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact => 'Kopiera kontakt till urklipp';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Ta bort kontakt';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Ta bort alla upptäckta kontakter';
|
||||
|
||||
@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';
|
||||
|
||||
@override
|
||||
String get translation_title => 'Översättning';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Aktivera översättning';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Översätt inkommande meddelanden och möjliggör översättning före avsändning.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Översätt innan du skickar';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Styr standardtillståndet för kompositorns översättningsikon.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Målmedvetet språk';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => 'Använd appens språk';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Nedladdad modell';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel =>
|
||||
'Fördefinierat Hugging Face-modell';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => 'Manualens URL';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Ladda ner modellen';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Nedladdning...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Arbeta...';
|
||||
|
||||
@override
|
||||
String get translation_stop => 'Stopp';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Slå samman de nedladdade delarna till en slutlig fil...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Nedladdade modeller';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Ta bort modell';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded =>
|
||||
'Översättningsmodellen har laddats ner.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'Nedladdningen avbruten.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'Nedladdning misslyckades: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst =>
|
||||
'Ange först en URL för en specifik modell.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Visa PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingHidePin => 'Dölj PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingPinTitle => 'Bluetooth‑parnings‑PIN';
|
||||
|
||||
@override
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Ange PIN för $deviceName (lämna tomt om ingen).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Meddelandets översättning';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending => 'Översätt innan du skickar';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'Meddelandena kommer att översättas innan de skickas.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Skicka meddelanden på det ursprungliga, stavade språket.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Översätt till $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Översättningsalternativ';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Språk för systemet';
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get common_delete => 'Видалити';
|
||||
|
||||
@override
|
||||
String get common_deleteAll => 'Видалити все';
|
||||
|
||||
@override
|
||||
String get common_close => 'Закрити';
|
||||
|
||||
@@ -108,6 +111,135 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTcpLabel => 'TCP';
|
||||
|
||||
@override
|
||||
String get tcpScreenTitle => 'З\'єднатися через протокол TCP';
|
||||
|
||||
@override
|
||||
String get tcpHostLabel => 'IP-адреса';
|
||||
|
||||
@override
|
||||
String get tcpHostHint => '192.168.40.10';
|
||||
|
||||
@override
|
||||
String get tcpPortLabel => 'Порт';
|
||||
|
||||
@override
|
||||
String get tcpPortHint => '5000';
|
||||
|
||||
@override
|
||||
String get tcpStatus_notConnected => 'Введіть кінцеву точку та підключіться';
|
||||
|
||||
@override
|
||||
String tcpStatus_connectingTo(String endpoint) {
|
||||
return 'Підключення до $endpoint...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tcpErrorHostRequired => 'Необхідно вказати IP-адресу.';
|
||||
|
||||
@override
|
||||
String get tcpErrorPortInvalid => 'Порт повинен бути в межах від 1 до 65535.';
|
||||
|
||||
@override
|
||||
String get tcpErrorUnsupported =>
|
||||
'Транспорт TCP не підтримується на цій платформі.';
|
||||
|
||||
@override
|
||||
String get tcpErrorTimedOut =>
|
||||
'З\'єднання TCP завершилося через закінчення часу очікування.';
|
||||
|
||||
@override
|
||||
String tcpConnectionFailed(String error) {
|
||||
return 'Не вдалося встановити з\'єднання TCP: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Підключити через USB';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Виберіть виявлене серійне пристрій і підключіть його безпосередньо до вашого вузла MeshCore.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Виберіть пристрій USB';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'USB-серіальний інтерфейс активний на підтримуваних пристроях на базі Android та на десктопних платформах.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Не знайдено жодних пристроїв USB. Підключіть один і перезавантажте.';
|
||||
|
||||
@override
|
||||
String get usbErrorPermissionDenied =>
|
||||
'Було відмовлено у наданні дозволу на використання USB.';
|
||||
|
||||
@override
|
||||
String get usbErrorDeviceMissing => 'Вибране USB-пристрій більше недоступне.';
|
||||
|
||||
@override
|
||||
String get usbErrorInvalidPort => 'Виберіть дійсний USB-пристрій.';
|
||||
|
||||
@override
|
||||
String get usbErrorBusy =>
|
||||
'Ще один запит на підключення через USB вже обробляється.';
|
||||
|
||||
@override
|
||||
String get usbErrorNotConnected => 'Немає підключених пристроїв USB.';
|
||||
|
||||
@override
|
||||
String get usbErrorOpenFailed => 'Не вдалося відкрити вибране USB-пристрій.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectFailed =>
|
||||
'Не вдалося підключитися до вибраного USB-пристрою.';
|
||||
|
||||
@override
|
||||
String get usbErrorUnsupported =>
|
||||
'Підтримка USB-серіального інтерфейсу не реалізована на цій платформі.';
|
||||
|
||||
@override
|
||||
String get usbErrorAlreadyActive => 'USB-з\'єднання вже встановлено.';
|
||||
|
||||
@override
|
||||
String get usbErrorNoDeviceSelected =>
|
||||
'Не було вибрано жодного пристрою USB.';
|
||||
|
||||
@override
|
||||
String get usbErrorPortClosed => 'З\'єднання USB не встановлено.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectTimedOut =>
|
||||
'З\'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion.';
|
||||
|
||||
@override
|
||||
String get usbFallbackDeviceName =>
|
||||
'Пристрій для передачі даних по веб-серіалах';
|
||||
|
||||
@override
|
||||
String get usbStatus_notConnected => 'Виберіть пристрій USB';
|
||||
|
||||
@override
|
||||
String get usbStatus_connecting => 'Підключення до USB-пристрою...';
|
||||
|
||||
@override
|
||||
String get usbStatus_searching => 'Пошук пристроїв USB...';
|
||||
|
||||
@override
|
||||
String usbConnectionFailed(String error) {
|
||||
return 'Не вдалося встановити з\'єднання через USB: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Пошук пристроїв...';
|
||||
|
||||
@@ -150,6 +282,13 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Потрібен браузер Chrome';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Увімкніть Bluetooth';
|
||||
|
||||
@@ -232,6 +371,13 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get settings_longitude => 'Довгота';
|
||||
|
||||
@override
|
||||
String get settings_contactSettings => 'Налаштування контактів';
|
||||
|
||||
@override
|
||||
String get settings_contactSettingsSubtitle =>
|
||||
'Налаштування для додавання контактів';
|
||||
|
||||
@override
|
||||
String get settings_privacyMode => 'Режим приватності';
|
||||
|
||||
@@ -249,6 +395,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 => 'Дії';
|
||||
|
||||
@@ -546,6 +736,49 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String get appSettings_autoRouteRotationDisabled =>
|
||||
'Авторотація маршрутизації вимкнена';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeight => 'Максимальна вага маршруту';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeightSubtitle =>
|
||||
'Максимальна вага, яку може накопичити маршрут завдяки успішним доставкам.';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeight => 'Початкова вартість маршруту';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeightSubtitle =>
|
||||
'Початкова вага для нових відкритих шляхів';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrement =>
|
||||
'Збільшення ваги успіху';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||
'Вага, додана до маршруту після успішної доставки';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrement =>
|
||||
'Зменшення ваги помилки';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||
'Вага, яка була знята з маршруту після невдалої доставки';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetries =>
|
||||
'Максимальна кількість повторних спроб надсилання повідомлення';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetriesSubtitle =>
|
||||
'Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => 'Батарея';
|
||||
|
||||
@@ -752,6 +985,9 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'Назва групи обов\'язкова.';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => 'Ця назва групи зарезервована';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'Група «$name» вже існує.';
|
||||
@@ -791,6 +1027,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 => 'Канали';
|
||||
|
||||
@@ -958,6 +1230,14 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => 'Поки немає повідомлень.';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => 'Надіслати повідомлення';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Надіслати повідомлення $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => 'Надішліть повідомлення, щоб почати';
|
||||
|
||||
@@ -978,11 +1258,6 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => 'Розташування';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return 'Надіслати повідомлення $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => 'Введіть повідомлення...';
|
||||
|
||||
@@ -1364,6 +1639,9 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Поділитися маркером тут';
|
||||
|
||||
@override
|
||||
String get map_setAsMyLocation => 'Встановити моє місцезнаходження';
|
||||
|
||||
@override
|
||||
String get map_pinLabel => 'Мітка піна';
|
||||
|
||||
@@ -1409,6 +1687,9 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get map_otherNodes => 'Інші вузли';
|
||||
|
||||
@override
|
||||
String get map_showOverlaps => 'Перекриття ключа повторювача';
|
||||
|
||||
@override
|
||||
String get map_keyPrefix => 'Префікс ключа';
|
||||
|
||||
@@ -1424,6 +1705,16 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get map_showSharedMarkers => 'Показувати спільні маркери';
|
||||
|
||||
@override
|
||||
String get map_showGuessedLocations =>
|
||||
'Показати місцезнаходження передбачених вузлів';
|
||||
|
||||
@override
|
||||
String get map_showDiscoveryContacts => 'Показати контакти Відкриття';
|
||||
|
||||
@override
|
||||
String get map_guessedLocation => 'Визначено місцезнаходження';
|
||||
|
||||
@override
|
||||
String get map_lastSeenTime => 'Час останньої активності';
|
||||
|
||||
@@ -1442,6 +1733,9 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get map_runTrace => 'Виконати трасування шляху';
|
||||
|
||||
@override
|
||||
String get map_runTraceWithReturnPath => 'Повернутися назад тим же шляхом';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Видалити останній';
|
||||
|
||||
@@ -1720,9 +2014,18 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => 'Адміністрування сервера кімнати';
|
||||
|
||||
@override
|
||||
String get repeater_guest => 'Інформація про ретранслятор';
|
||||
|
||||
@override
|
||||
String get room_guest => 'Інформація про сервер кімнати';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Інструменти керування';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => 'Інструменти для гостей';
|
||||
|
||||
@override
|
||||
String get repeater_status => 'Статус';
|
||||
|
||||
@@ -1756,6 +2059,13 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_settingsSubtitle => 'Налаштувати параметри ретранслятора';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin => 'Синхронізація годин після входу';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle =>
|
||||
'Автоматично надсилати повідомлення \"синхронізація годин\" після успішного входу.';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => 'Статус ретранслятора';
|
||||
|
||||
@@ -2136,6 +2446,12 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => 'Годинник';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => 'Синхронізація годинника';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => 'Відкрити сусідів';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => 'Надсилає пакет оголошення';
|
||||
|
||||
@@ -3133,4 +3449,276 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Останній раз бачили';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Налаштування контактів';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Автоматичне виявлення';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Інші налаштування, пов\'язані з контактами';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Автоматично додавати користувачів';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Дозволити супутникові автоматично додавати виявлених користувачів';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Автоматично додавати повторювачі';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Дозволити супутнику автоматично додавати виявлені ретранслятори';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Автоматично додавати сервери кімнат';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Дозволити супровіднику автоматично додавати виявлені сервери кімнат.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Автоматично додавати датчики';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Дозволити супровіднику автоматично додавати виявлені сенсори';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle => 'Перезаписати найстаріше';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'Коли список контактів заповнений, найстарший контакт без позначки улюбленого буде замінений.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Виявлені контакти';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching =>
|
||||
'Відповідних контактів не знайдено';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Знайти виявлені контакти';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Контакт додано';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Додати контакт';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact =>
|
||||
'Копіювати контакт у буфер обміну';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Видалити контакт';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Видалити всі виявлені контакти';
|
||||
|
||||
@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 та час, протягом якого пристрій використовує радіоканал.';
|
||||
|
||||
@override
|
||||
String get translation_title => 'Переклад';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Увімкнути переклад';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle =>
|
||||
'Перекладати отримані повідомлення та дозволяти попередній переклад перед відправкою.';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => 'Перекладіть перед відправкою';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle =>
|
||||
'Контролює стан ікон перекладу, який використовується за замовчуванням.';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => 'Цільова мова';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => 'Використовуйте мову додатку';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => 'Завантажений шаблон';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel =>
|
||||
'Заздалегідь налаштований модель від Hugging Face';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel =>
|
||||
'Посилання на веб-сторінку з інструкцією';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => 'Завантажити модель';
|
||||
|
||||
@override
|
||||
String get translation_downloading => 'Завантаження...';
|
||||
|
||||
@override
|
||||
String get translation_working => 'Працюю...';
|
||||
|
||||
@override
|
||||
String get translation_stop => 'Припинити';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks =>
|
||||
'Об\'єднання завантажених фрагментів у кінцевий файл...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => 'Завантажені моделі';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => 'Видалити модель';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded => 'Модель перекладу завантажена.';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => 'Завантаження призупинено.';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return 'Не вдалося завантажити: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst => 'Спочатку введіть URL моделі.';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => 'Показати PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingHidePin => 'Приховати PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingPinTitle => 'PIN‑код спарювання Bluetooth';
|
||||
|
||||
@override
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return 'Введіть PIN для $deviceName (залиште порожнім, якщо його немає).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => 'Переклад повідомлення';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending =>
|
||||
'Перекладіть перед відправкою';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint =>
|
||||
'Повідомлення будуть перекладені перед відправленням.';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint =>
|
||||
'Надсилайте повідомлення, використовуючи оригінальний текстовий формат.';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return 'Перекласти на $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => 'Варіанти перекладу';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => 'Мова системи';
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get common_delete => '删除';
|
||||
|
||||
@override
|
||||
String get common_deleteAll => '删除全部';
|
||||
|
||||
@override
|
||||
String get common_close => '关闭';
|
||||
|
||||
@@ -108,6 +111,123 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => '连接设备';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => '蓝牙';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTcpLabel => 'TCP';
|
||||
|
||||
@override
|
||||
String get tcpScreenTitle => '通过 TCP 连接';
|
||||
|
||||
@override
|
||||
String get tcpHostLabel => 'IP地址';
|
||||
|
||||
@override
|
||||
String get tcpHostHint => '192.168.40.10';
|
||||
|
||||
@override
|
||||
String get tcpPortLabel => '端口';
|
||||
|
||||
@override
|
||||
String get tcpPortHint => '5000';
|
||||
|
||||
@override
|
||||
String get tcpStatus_notConnected => '输入目标地址,然后连接';
|
||||
|
||||
@override
|
||||
String tcpStatus_connectingTo(String endpoint) {
|
||||
return '连接到 $endpoint...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tcpErrorHostRequired => '需要提供IP地址。';
|
||||
|
||||
@override
|
||||
String get tcpErrorPortInvalid => '端口号必须在 1 到 65535 之间。';
|
||||
|
||||
@override
|
||||
String get tcpErrorUnsupported => '此平台不支持 TCP 传输。';
|
||||
|
||||
@override
|
||||
String get tcpErrorTimedOut => 'TCP 连接超时。';
|
||||
|
||||
@override
|
||||
String tcpConnectionFailed(String error) {
|
||||
return 'TCP 连接失败:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => '通过USB连接';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle => '选择已检测到的串行设备,并直接连接到您的 MeshCore 节点。';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => '选择一个 USB 设备';
|
||||
|
||||
@override
|
||||
String get usbScreenNote => 'USB 串行接口在支持的 Android 设备和桌面平台上处于活动状态。';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState => '未找到任何 USB 设备。请插入一个,然后刷新。';
|
||||
|
||||
@override
|
||||
String get usbErrorPermissionDenied => '拒绝了USB权限。';
|
||||
|
||||
@override
|
||||
String get usbErrorDeviceMissing => '所选的USB设备已不再可用。';
|
||||
|
||||
@override
|
||||
String get usbErrorInvalidPort => '选择一个有效的USB设备。';
|
||||
|
||||
@override
|
||||
String get usbErrorBusy => '还有一个 USB 连接请求正在进行中。';
|
||||
|
||||
@override
|
||||
String get usbErrorNotConnected => '没有连接任何USB设备。';
|
||||
|
||||
@override
|
||||
String get usbErrorOpenFailed => '未能打开所选的USB设备。';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectFailed => '未能连接到所选的USB设备。';
|
||||
|
||||
@override
|
||||
String get usbErrorUnsupported => '此平台不支持USB串行通信。';
|
||||
|
||||
@override
|
||||
String get usbErrorAlreadyActive => 'USB 连接已建立。';
|
||||
|
||||
@override
|
||||
String get usbErrorNoDeviceSelected => '未选择任何 USB 设备。';
|
||||
|
||||
@override
|
||||
String get usbErrorPortClosed => 'USB 连接未建立。';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectTimedOut => '连接超时。请确保设备已安装 USB 伴侣固件。';
|
||||
|
||||
@override
|
||||
String get usbFallbackDeviceName => 'Web 串流设备';
|
||||
|
||||
@override
|
||||
String get usbStatus_notConnected => '选择一个 USB 设备';
|
||||
|
||||
@override
|
||||
String get usbStatus_connecting => '连接USB设备...';
|
||||
|
||||
@override
|
||||
String get usbStatus_searching => '正在搜索 USB 设备...';
|
||||
|
||||
@override
|
||||
String usbConnectionFailed(String error) {
|
||||
return 'USB 连接失败:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanner_scanning => '正在搜索设备...';
|
||||
|
||||
@@ -148,6 +268,13 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage => '请开启蓝牙以搜索设备';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => '需要 Chrome 浏览器';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'此 Web 应用程序需要 Google Chrome 或基于 Chromium 的浏览器以支持蓝牙。';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => '启用蓝牙';
|
||||
|
||||
@@ -226,6 +353,12 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get settings_longitude => '经度';
|
||||
|
||||
@override
|
||||
String get settings_contactSettings => '联系人设置';
|
||||
|
||||
@override
|
||||
String get settings_contactSettingsSubtitle => '添加联系人的设置';
|
||||
|
||||
@override
|
||||
String get settings_privacyMode => '隐私模式';
|
||||
|
||||
@@ -241,6 +374,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 => '操作';
|
||||
|
||||
@@ -515,6 +689,43 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_autoRouteRotationDisabled => '自动路径轮换已禁用';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeight => '最大路径重量';
|
||||
|
||||
@override
|
||||
String get appSettings_maxRouteWeightSubtitle => '一条路径可以累积的最大重量,取决于成功交付的数量。';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeight => '初始路线权重';
|
||||
|
||||
@override
|
||||
String get appSettings_initialRouteWeightSubtitle => '新发现路径的初始重量';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrement => '成功权重增加';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||
'在成功交付后,将重量添加到路径中';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrement => '失败权重降低';
|
||||
|
||||
@override
|
||||
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||
'从一条路径上移除的货物,由于无法成功交付而移除。';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetries => '最大消息重试次数';
|
||||
|
||||
@override
|
||||
String get appSettings_maxMessageRetriesSubtitle => '在将消息标记为失败之前,允许尝试的次数';
|
||||
|
||||
@override
|
||||
String path_routeWeight(String weight, String max) {
|
||||
return '$weight/$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_battery => '电池';
|
||||
|
||||
@@ -712,6 +923,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => '请输入群聊名称';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => '该群组名称已被保留';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return '名为 \"$name\" 的群聊已存在';
|
||||
@@ -750,6 +964,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 => '频道';
|
||||
|
||||
@@ -914,6 +1161,14 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get chat_noMessages => '暂无消息';
|
||||
|
||||
@override
|
||||
String get chat_sendMessage => '发送消息';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return '发送消息给 $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_sendMessageToStart => '发送消息开始对话';
|
||||
|
||||
@@ -933,11 +1188,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get chat_location => '位置';
|
||||
|
||||
@override
|
||||
String chat_sendMessageTo(String contactName) {
|
||||
return '发送消息给 $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_typeMessage => '输入消息...';
|
||||
|
||||
@@ -1288,6 +1538,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get map_shareMarkerHere => '在此分享标记';
|
||||
|
||||
@override
|
||||
String get map_setAsMyLocation => '设置为我的位置';
|
||||
|
||||
@override
|
||||
String get map_pinLabel => '标签';
|
||||
|
||||
@@ -1332,6 +1585,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get map_otherNodes => '其他节点';
|
||||
|
||||
@override
|
||||
String get map_showOverlaps => '重复键重叠';
|
||||
|
||||
@override
|
||||
String get map_keyPrefix => '关键字前缀';
|
||||
|
||||
@@ -1347,6 +1603,15 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get map_showSharedMarkers => '显示共享标记';
|
||||
|
||||
@override
|
||||
String get map_showGuessedLocations => '显示猜测的节点位置';
|
||||
|
||||
@override
|
||||
String get map_showDiscoveryContacts => '显示发现联系人';
|
||||
|
||||
@override
|
||||
String get map_guessedLocation => '猜测的位置';
|
||||
|
||||
@override
|
||||
String get map_lastSeenTime => '最后在线时间';
|
||||
|
||||
@@ -1365,6 +1630,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get map_runTrace => '运行路径追踪';
|
||||
|
||||
@override
|
||||
String get map_runTraceWithReturnPath => '沿着相同的路径返回';
|
||||
|
||||
@override
|
||||
String get map_removeLast => '移除最后一个';
|
||||
|
||||
@@ -1622,9 +1890,18 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get room_management => '房间服务器管理';
|
||||
|
||||
@override
|
||||
String get repeater_guest => '重复器信息';
|
||||
|
||||
@override
|
||||
String get room_guest => '服务器信息';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => '管理工具';
|
||||
|
||||
@override
|
||||
String get repeater_guestTools => '访客工具';
|
||||
|
||||
@override
|
||||
String get repeater_status => '状态';
|
||||
|
||||
@@ -1655,6 +1932,12 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_settingsSubtitle => '配置转发节点参数';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLogin => '登录后,自动同步时钟';
|
||||
|
||||
@override
|
||||
String get repeater_clockSyncAfterLoginSubtitle => '在成功登录后,自动发送“时钟同步”指令。';
|
||||
|
||||
@override
|
||||
String get repeater_statusTitle => '转发节点状态';
|
||||
|
||||
@@ -2012,6 +2295,12 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get repeater_cliQuickClock => '时钟';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickClockSync => '同步时钟';
|
||||
|
||||
@override
|
||||
String get repeater_cliQuickDiscovery => '发现邻居';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpAdvert => '发送广播包';
|
||||
|
||||
@@ -2893,4 +3182,249 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => '最近访问';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => '联系人设置';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => '自动发现';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle => '其他联系人相关设置';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle => '自动添加用户';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle => '允许伴侣自动添加发现的用户';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle => '自动添加重复器';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle => '允许伴侣自动添加发现的重复器';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle => '自动添加房间服务器';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle => '允许伴侣自动添加发现的房间服务器';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle => '自动添加传感器';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle => '允许伴侣自动添加发现的传感器';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle => '覆盖最旧的';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'当联系人列表已满时,将替换最老的非收藏联系人。';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => '已发现的联系人';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => '没有匹配的联系人';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => '搜索已发现的联系人';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => '联系人已添加';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => '添加联系人';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact => '复制联系人到剪贴板';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => '删除联系人';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll => '删除所有发现的联系人';
|
||||
|
||||
@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、信噪比和空中时间';
|
||||
|
||||
@override
|
||||
String get translation_title => '翻译';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => '启用翻译功能';
|
||||
|
||||
@override
|
||||
String get translation_enableSubtitle => '翻译收到的消息,并允许在发送前进行翻译。';
|
||||
|
||||
@override
|
||||
String get translation_composerTitle => '在发送之前进行翻译';
|
||||
|
||||
@override
|
||||
String get translation_composerSubtitle => '控制作曲家翻译图标的默认状态。';
|
||||
|
||||
@override
|
||||
String get translation_targetLanguage => '目标语言';
|
||||
|
||||
@override
|
||||
String get translation_useAppLanguage => '使用应用程序语言';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModelLabel => '下载的模型';
|
||||
|
||||
@override
|
||||
String get translation_presetModelLabel => '预设的 Hugging Face 模型';
|
||||
|
||||
@override
|
||||
String get translation_manualUrlLabel => '手动模型网址';
|
||||
|
||||
@override
|
||||
String get translation_downloadModel => '下载模型';
|
||||
|
||||
@override
|
||||
String get translation_downloading => '正在下载...';
|
||||
|
||||
@override
|
||||
String get translation_working => '工作中...';
|
||||
|
||||
@override
|
||||
String get translation_stop => '停止';
|
||||
|
||||
@override
|
||||
String get translation_mergingChunks => '将下载的片段合并成最终文件...';
|
||||
|
||||
@override
|
||||
String get translation_downloadedModels => '下载的模型';
|
||||
|
||||
@override
|
||||
String get translation_deleteModel => '删除模型';
|
||||
|
||||
@override
|
||||
String get translation_modelDownloaded => '翻译模型已下载。';
|
||||
|
||||
@override
|
||||
String get translation_downloadStopped => '下载已停止。';
|
||||
|
||||
@override
|
||||
String translation_downloadFailed(String error) {
|
||||
return '下载失败:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_enterUrlFirst => '首先,请输入模型的 URL。';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingShowPin => '显示PIN码';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingHidePin => '隐藏 PIN';
|
||||
|
||||
@override
|
||||
String get scanner_linuxPairingPinTitle => '蓝牙配对 PIN';
|
||||
|
||||
@override
|
||||
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||
return '输入 $deviceName 的 PIN 码(如果为空,则留空)。';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_messageTranslation => '消息翻译';
|
||||
|
||||
@override
|
||||
String get translation_translateBeforeSending => '在发送前进行翻译';
|
||||
|
||||
@override
|
||||
String get translation_composerEnabledHint => '消息将在发送前进行翻译。';
|
||||
|
||||
@override
|
||||
String get translation_composerDisabledHint => '使用原始的打字方式发送消息。';
|
||||
|
||||
@override
|
||||
String translation_translateTo(String language) {
|
||||
return '翻译成 $language';
|
||||
}
|
||||
|
||||
@override
|
||||
String get translation_translationOptions => '翻译选项';
|
||||
|
||||
@override
|
||||
String get translation_systemLanguage => '系统语言';
|
||||
}
|
||||
|
||||
+321
-47
@@ -84,7 +84,7 @@
|
||||
"settings_appSettings": "App Instellingen",
|
||||
"settings_appSettingsSubtitle": "Notificaties, berichten en kaartinstellingen",
|
||||
"settings_nodeSettings": "Node Instellingen",
|
||||
"settings_nodeName": "Node Naam",
|
||||
"settings_nodeName": "Nodenaam",
|
||||
"settings_nodeNameNotSet": "Niet ingesteld",
|
||||
"settings_nodeNameHint": "Voer nodenaam in",
|
||||
"settings_nodeNameUpdated": "Naam bijgewerkt",
|
||||
@@ -107,13 +107,13 @@
|
||||
"settings_sendAdvertisement": "Verzend Advertentie",
|
||||
"settings_sendAdvertisementSubtitle": "Nu aanwezigheid uitzenden",
|
||||
"settings_advertisementSent": "Advertentie verzonden",
|
||||
"settings_syncTime": "Synchronisatie Tijd",
|
||||
"settings_syncTime": "Tijd Synchroniseren",
|
||||
"settings_syncTimeSubtitle": "Stel de apparaatklok in op de tijd van de telefoon.",
|
||||
"settings_timeSynchronized": "Tijdsynchronisatie",
|
||||
"settings_refreshContacts": "Contacten vernieuwen",
|
||||
"settings_refreshContactsSubtitle": "Contactlijst opnieuw laden van het apparaat",
|
||||
"settings_rebootDevice": "Apparaat opnieuw opstarten",
|
||||
"settings_rebootDeviceSubtitle": "Herstart het MeshCore apparaat",
|
||||
"settings_rebootDeviceSubtitle": "Herstart het MeshCore-apparaat",
|
||||
"settings_rebootDeviceConfirm": "Ben je er zeker van dat je het apparaat opnieuw wilt opstarten? Je wordt losgekoppeld.",
|
||||
"settings_debug": "Debug",
|
||||
"settings_bleDebugLog": "BLE Debug Log",
|
||||
@@ -145,7 +145,7 @@
|
||||
"settings_bandwidth": "Bandbreedte",
|
||||
"settings_spreadingFactor": "Spreadsnelheid",
|
||||
"settings_codingRate": "Codeertarief",
|
||||
"settings_txPower": "TX Vermogen (dBm)",
|
||||
"settings_txPower": "TX-Vermogen (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Ongeldige TX-vermogen (0-22 dBm)",
|
||||
"settings_error": "Fout: {message}",
|
||||
@@ -232,11 +232,11 @@
|
||||
"appSettings_mapTimeFilter": "Filter tijd op kaart",
|
||||
"appSettings_showNodesDiscoveredWithin": "Toon nodes ontdekt binnen:",
|
||||
"appSettings_allTime": "Altijd",
|
||||
"appSettings_lastHour": "Laat uur",
|
||||
"appSettings_last6Hours": "laatste 6 uur",
|
||||
"appSettings_last24Hours": "De laatste 24 uur",
|
||||
"appSettings_lastWeek": "Laatste week",
|
||||
"appSettings_offlineMapCache": "Offline Kaarten Cache",
|
||||
"appSettings_lastHour": "Afgelopen uur",
|
||||
"appSettings_last6Hours": "Afgelopen 6 uur",
|
||||
"appSettings_last24Hours": "Afgelopen 24 uur",
|
||||
"appSettings_lastWeek": "Afgelopen week",
|
||||
"appSettings_offlineMapCache": "Offline Kaartcache",
|
||||
"appSettings_noAreaSelected": "Geen gebied geselecteerd",
|
||||
"appSettings_areaSelectedZoom": "Geselecteerd gebied (zoom {minZoom}-{maxZoom})",
|
||||
"@appSettings_areaSelectedZoom": {
|
||||
@@ -285,6 +285,7 @@
|
||||
"contacts_newGroup": "Nieuwe Groep",
|
||||
"contacts_groupName": "Groepnaam",
|
||||
"contacts_groupNameRequired": "De groepnaam is verplicht.",
|
||||
"contacts_groupNameReserved": "Deze groepsnaam is gereserveerd",
|
||||
"contacts_groupAlreadyExists": "De groep \"{name}\" bestaat al.",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -681,7 +682,7 @@
|
||||
"map_showSharedMarkers": "Toon gedeelde markeringen",
|
||||
"map_lastSeenTime": "Laatste Bekeken Tijd",
|
||||
"map_sharedPin": "Gedeelde pin",
|
||||
"map_joinRoom": "Sluit Kamer",
|
||||
"map_joinRoom": "Kamer Toetreden",
|
||||
"map_manageRepeater": "Beheer Repeater",
|
||||
"mapCache_title": "Offline Kaarten Cache",
|
||||
"mapCache_selectAreaFirst": "Select een gebied om eerst in de cache op te slaan",
|
||||
@@ -877,7 +878,7 @@
|
||||
"path_tooLong": "Pad is te lang. Maximaal 64 sprongen zijn toegestaan.",
|
||||
"path_setPath": "Stel Pad in",
|
||||
"repeater_management": "Beheer Repeaters",
|
||||
"repeater_managementTools": "Beheerinstrumenten",
|
||||
"repeater_managementTools": "Beheerfuncties",
|
||||
"repeater_status": "Status",
|
||||
"repeater_statusSubtitle": "Status, statistieken en buren bekijken",
|
||||
"repeater_telemetry": "Telemetry",
|
||||
@@ -911,8 +912,8 @@
|
||||
"repeater_lastRssi": "Laatste RSSI",
|
||||
"repeater_lastSnr": "Laatste SNR",
|
||||
"repeater_noiseFloor": "Ruisvloer",
|
||||
"repeater_txAirtime": "TX Airtime",
|
||||
"repeater_rxAirtime": "RX Airtime",
|
||||
"repeater_txAirtime": "TX-zendtijd",
|
||||
"repeater_rxAirtime": "RX-zendtijd",
|
||||
"repeater_packetStatistics": "Pakketstatistieken",
|
||||
"repeater_sent": "Verzonden",
|
||||
"repeater_received": "Ontvangen",
|
||||
@@ -981,14 +982,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_settingsTitle": "Repeater Instellingen",
|
||||
"repeater_settingsTitle": "Repeaterinstellingen",
|
||||
"repeater_basicSettings": "Basisinstellingen",
|
||||
"repeater_repeaterName": "Repeaternaam",
|
||||
"repeater_repeaterNameHelper": "Weergave naam voor deze repeater",
|
||||
"repeater_repeaterNameHelper": "Weergavenaam voor deze repeater",
|
||||
"repeater_adminPassword": "Admin wachtwoord",
|
||||
"repeater_adminPasswordHelper": "Volledige toegangspaswoord",
|
||||
"repeater_guestPassword": "Wachtwoord Gast",
|
||||
"repeater_guestPasswordHelper": "Leesbeheer wachtwoord",
|
||||
"repeater_adminPasswordHelper": "Wachtwoord administratortoegang",
|
||||
"repeater_guestPassword": "Gast wachtwoord",
|
||||
"repeater_guestPasswordHelper": "Wachtwoord gasttoegen",
|
||||
"repeater_radioSettings": "Radio Instellingen",
|
||||
"repeater_frequencyMhz": "Frequentie (MHz)",
|
||||
"repeater_frequencyHelper": "300-2500 MHz",
|
||||
@@ -997,7 +998,7 @@
|
||||
"repeater_bandwidth": "Bandbreedte",
|
||||
"repeater_spreadingFactor": "Spreidingsfactor",
|
||||
"repeater_codingRate": "Codeertarief",
|
||||
"repeater_locationSettings": "Locatie Instellingen",
|
||||
"repeater_locationSettings": "Locatie-instellingen",
|
||||
"repeater_latitude": "Breedtegraad",
|
||||
"repeater_latitudeHelper": "Graadseconden (bijv. 37.7749)",
|
||||
"repeater_longitude": "Lengtegraad",
|
||||
@@ -1007,9 +1008,9 @@
|
||||
"repeater_packetForwardingSubtitle": "Repeater instellen om pakketten door te sturen",
|
||||
"repeater_guestAccess": "Toegang voor Gasten",
|
||||
"repeater_guestAccessSubtitle": "Toegestane leesbeheer toegang voor gasten.",
|
||||
"repeater_privacyMode": "Privacy Modus",
|
||||
"repeater_privacyMode": "Privacymodus",
|
||||
"repeater_privacyModeSubtitle": "Naam/locatie verbergen in advertenties",
|
||||
"repeater_advertisementSettings": "Advertentie Instellingen",
|
||||
"repeater_advertisementSettings": "Advertentie-instellingen",
|
||||
"repeater_localAdvertInterval": "Lokale Advertentie Interval",
|
||||
"repeater_localAdvertIntervalMinutes": "{minutes} minuten",
|
||||
"@repeater_localAdvertIntervalMinutes": {
|
||||
@@ -1072,7 +1073,7 @@
|
||||
"repeater_refreshLocationSettings": "Instellingen Locatie Vernieuwen",
|
||||
"repeater_refreshPacketForwarding": "Vernieuwen Pakket Doorversturing",
|
||||
"repeater_refreshGuestAccess": "Toegang Gast Vernieuwen",
|
||||
"repeater_refreshPrivacyMode": "Privacy Mode vernieuwen",
|
||||
"repeater_refreshPrivacyMode": "Privacymode vernieuwen",
|
||||
"repeater_refreshAdvertisementSettings": "Instellingen Advertentie Bijwerken",
|
||||
"repeater_refreshed": "{label} is vernieuwd",
|
||||
"@repeater_refreshed": {
|
||||
@@ -1093,8 +1094,8 @@
|
||||
"repeater_cliTitle": "Repeater CLI",
|
||||
"repeater_debugNextCommand": "Debug Volgende Commando",
|
||||
"repeater_commandHelp": "Help",
|
||||
"repeater_clearHistory": "Verwijder Geschiedenis",
|
||||
"repeater_noCommandsSent": "Geen commando's verzonden nog.",
|
||||
"repeater_clearHistory": "Geschiedenis Verwijderen",
|
||||
"repeater_noCommandsSent": "Nog geen commando's verzonden.",
|
||||
"repeater_typeCommandOrUseQuick": "Typ een opdracht hieronder of gebruik snelle commando's",
|
||||
"repeater_enterCommandHint": "Voer bevel in...",
|
||||
"repeater_previousCommand": "Vorige opdracht",
|
||||
@@ -1109,14 +1110,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_cliQuickGetName": "Haal Naam op",
|
||||
"repeater_cliQuickGetRadio": "Radio ontvangen",
|
||||
"repeater_cliQuickGetTx": "Krijg TX",
|
||||
"repeater_cliQuickNeighbors": "Buren",
|
||||
"repeater_cliQuickVersion": "Versie",
|
||||
"repeater_cliQuickAdvertise": "Advertenties",
|
||||
"repeater_cliQuickClock": "Tijd",
|
||||
"repeater_cliHelpAdvert": "Verstuurt een advertentiepakket",
|
||||
"repeater_cliQuickGetName": "Naam opvragen",
|
||||
"repeater_cliQuickGetRadio": "Radio-instellingen opvragen",
|
||||
"repeater_cliQuickGetTx": "TX opvragen",
|
||||
"repeater_cliQuickNeighbors": "Buren opvragen",
|
||||
"repeater_cliQuickVersion": "Versie opvragen",
|
||||
"repeater_cliQuickAdvertise": "Advertenties opvragen",
|
||||
"repeater_cliQuickClock": "Tijd opvragen",
|
||||
"repeater_cliHelpAdvert": "Advertentie uitzenden",
|
||||
"repeater_cliHelpReboot": "Herstart het apparaat. (let op, je krijgt mogelijk een 'Timeout', wat normaal is)",
|
||||
"repeater_cliHelpClock": "Toont de huidige tijd per apparaat's klok.",
|
||||
"repeater_cliHelpPassword": "Stelt een nieuw beheerderswachtwoord in voor het apparaat.",
|
||||
@@ -1202,7 +1203,7 @@
|
||||
},
|
||||
"telemetry_batteryLabel": "Batterij",
|
||||
"telemetry_voltageLabel": "Spanning",
|
||||
"telemetry_mcuTemperatureLabel": "MCU Temperatuur",
|
||||
"telemetry_mcuTemperatureLabel": "MCU-temperatuur",
|
||||
"telemetry_temperatureLabel": "Temperatuur",
|
||||
"telemetry_currentLabel": "Huidig",
|
||||
"telemetry_batteryValue": "{percent}% / {volts}V",
|
||||
@@ -1345,7 +1346,7 @@
|
||||
"listFilter_tooltip": "Filteren en sorteren",
|
||||
"listFilter_sortBy": "Sorteren door",
|
||||
"listFilter_latestMessages": "Recente berichten",
|
||||
"listFilter_heardRecently": "Hoor je onlangs",
|
||||
"listFilter_heardRecently": "Recent gezien",
|
||||
"listFilter_az": "A-Z",
|
||||
"listFilter_filters": "Filters",
|
||||
"listFilter_all": "Alles",
|
||||
@@ -1363,20 +1364,20 @@
|
||||
}
|
||||
},
|
||||
"repeater_neighbors": "Buren",
|
||||
"repeater_neighborsSubtitle": "Bekijk nul hops buren.",
|
||||
"repeater_neighborsSubtitle": "Bekijk nul-hopsburen.",
|
||||
"neighbors_receivedData": "Ontvangen Buurdata",
|
||||
"neighbors_requestTimedOut": "Buren vragen om tijdelijk uitgeschakeld.",
|
||||
"neighbors_errorLoading": "Fout bij het laden van buren: {error}",
|
||||
"neighbors_repeatersNeighbors": "Herhalingen Buren",
|
||||
"neighbors_repeatersNeighbors": "Repeatbburen",
|
||||
"neighbors_noData": "Geen gegevens van buren beschikbaar.",
|
||||
"channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.",
|
||||
"channels_createPrivateChannel": "Maak een Privé Kanaal",
|
||||
"channels_joinPrivateChannel": "Sluit een Privé Kanaal aan",
|
||||
"channels_joinPrivateChannelDesc": "Handmatig een geheime sleutel invoeren.",
|
||||
"channels_joinPublicChannel": "Sluit het Open Kanaal",
|
||||
"channels_joinPublicChannelDesc": "Iedereen kan dit kanaal aanmelden.",
|
||||
"channels_joinHashtagChannel": "Sluit een Hashtag Kanaal",
|
||||
"channels_joinHashtagChannelDesc": "Iedereen kan lid worden van hashtag-kanalen.",
|
||||
"channels_createPrivateChannel": "PrivéKanaal Aanmaken",
|
||||
"channels_joinPrivateChannel": "PrivéKanaal Toetreden",
|
||||
"channels_joinPrivateChannelDesc": "Voer handmatig een geheime sleutel in.",
|
||||
"channels_joinPublicChannel": "Publiek Kanaal Toetreden",
|
||||
"channels_joinPublicChannelDesc": "Iedereen kan toetreden tot dit kanaal.",
|
||||
"channels_joinHashtagChannel": "Hashtag-kanaal Aanmaken",
|
||||
"channels_joinHashtagChannelDesc": "Iedereen kan toetreden tot hashtag-kanalen.",
|
||||
"channels_scanQrCode": "Scan een QR-code",
|
||||
"channels_scanQrCodeComingSoon": "Komt later",
|
||||
"channels_enterHashtag": "Voer hashtag in",
|
||||
@@ -1574,7 +1575,7 @@
|
||||
"contacts_zeroHopContactAdvertSent": "Contact verzonden via advertentie",
|
||||
"contacts_contactAdvertCopied": "Reclame gekopieerd naar Klembord.",
|
||||
"contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.",
|
||||
"contacts_ShareContact": "Kontakt naar Klembord kopiëren",
|
||||
"contacts_ShareContact": "Contact naar Klembord kopiëren",
|
||||
"contacts_ShareContactZeroHop": "Contact delen via advertentie",
|
||||
"contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden",
|
||||
"notification_activityTitle": "MeshCore Activiteit",
|
||||
@@ -1606,12 +1607,14 @@
|
||||
"map_runTrace": "Padeshulp traceren",
|
||||
"scanner_enableBluetooth": "Activeer Bluetooth",
|
||||
"scanner_bluetoothOffMessage": "Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.",
|
||||
"scanner_chromeRequired": "Chrome-browser vereist",
|
||||
"scanner_chromeRequiredMessage": "Deze webapplicatie vereist Google Chrome of een op Chromium gebaseerde browser voor Bluetooth-ondersteuning.",
|
||||
"scanner_bluetoothOff": "Bluetooth is uitgeschakeld",
|
||||
"snrIndicator_lastSeen": "Laatst gezien",
|
||||
"snrIndicator_nearByRepeaters": "Nabije herhalingseenheden",
|
||||
"chat_ShowAllPaths": "Toon alle paden",
|
||||
"settings_clientRepeat": "Herhalen: Afgekoppeld",
|
||||
"settings_clientRepeatSubtitle": "Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.",
|
||||
"settings_clientRepeat": "Off-Grid Herhalen",
|
||||
"settings_clientRepeatSubtitle": "Laat dit apparaat de berichten van andere apparaten doorsturen.",
|
||||
"settings_clientRepeatFreqWarning": "Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist.",
|
||||
"settings_aboutOpenMeteoAttribution": "LOS-hoogtegegevens: Open-Meteo (CC BY 4.0)",
|
||||
"appSettings_unitsTitle": "Eenheden",
|
||||
@@ -1800,5 +1803,276 @@
|
||||
"contacts_searchContactsNoNumber": "Zoek contacten...",
|
||||
"contacts_searchUsers": "Zoek {number}{str} gebruikers...",
|
||||
"contacts_searchFavorites": "Zoek {number}{str} favorieten...",
|
||||
"contacts_searchRoomServers": "Zoek {number}{str} Room servers..."
|
||||
"contacts_searchRoomServers": "Zoek {number}{str} Room servers...",
|
||||
"contactsSettings_autoAddUsersTitle": "Gebruikers automatisch toevoegen",
|
||||
"contactsSettings_title": "Instellingen voor contacten",
|
||||
"settings_contactSettings": "Contactinstellingen",
|
||||
"contactsSettings_otherTitle": "Andere instellingen voor contactgerelateerde zaken",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "Sta toe dat de companion automatisch ontdekte repeaters toevoegt",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Automatisch kamerservers toevoegen",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Sta toe dat de companion automatisch ontdekte kamer servers toevoegt.",
|
||||
"contactsSettings_autoAddSensorsTitle": "Automatisch sensoren toevoegen",
|
||||
"settings_contactSettingsSubtitle": "Instellingen voor het toevoegen van contacten",
|
||||
"contactsSettings_autoAddTitle": "Automatische detectie",
|
||||
"contactsSettings_autoAddSensorsSubtitle": "Sta toe dat de companion automatisch ontdekte sensoren toevoegt",
|
||||
"contactsSettings_autoAddUsersSubtitle": "Sta toe dat de companion automatisch ontdekte gebruikers toevoegt",
|
||||
"contactsSettings_autoAddRepeatersTitle": "Automatisch herhalingstoestellen toevoegen",
|
||||
"contactsSettings_overwriteOldestTitle": "Overschrijf Oudste",
|
||||
"discoveredContacts_noMatching": "Geen overeenkomende contacten",
|
||||
"discoveredContacts_addContact": "Contact toevoegen",
|
||||
"discoveredContacts_copyContact": "Kopieer contact naar klembord",
|
||||
"discoveredContacts_deleteContact": "Contact verwijderen",
|
||||
"discoveredContacts_Title": "Ontdekte contacten",
|
||||
"discoveredContacts_contactAdded": "Contact toegevoegd",
|
||||
"discoveredContacts_searchHint": "Ontdekte contacten zoeken",
|
||||
"contactsSettings_overwriteOldestSubtitle": "Wanneer de contactenlijst vol is, wordt de oudste niet-favoriete contactpersoon vervangen.",
|
||||
"common_deleteAll": "Alles verwijderen",
|
||||
"discoveredContacts_deleteContactAll": "Verwijder alle ontdekte contacten",
|
||||
"discoveredContacts_deleteContactAllContent": "Weet u zeker dat u alle ontdekte contacten wilt verwijderen?",
|
||||
"map_guessedLocation": "Geroerde locatie",
|
||||
"map_showGuessedLocations": "Toon de voorspelde locaties van de knopen",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"usbScreenSubtitle": "Selecteer een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"usbScreenTitle": "Verbind via USB",
|
||||
"usbScreenStatus": "Selecteer een USB-apparaat",
|
||||
"usbScreenNote": "USB-serieel is actief op ondersteunde Android-apparaten en desktop-platforms.",
|
||||
"usbScreenEmptyState": "Geen USB-apparaten gevonden. Sluit er een aan en herlaad.",
|
||||
"usbErrorPermissionDenied": "Toegang via USB is geweigerd.",
|
||||
"usbErrorDeviceMissing": "Het geselecteerde USB-apparaat is niet meer beschikbaar.",
|
||||
"usbErrorInvalidPort": "Selecteer een geldig USB-apparaat.",
|
||||
"usbErrorBusy": "Een andere verzoek om een USB-verbinding is al in behandeling.",
|
||||
"usbErrorNotConnected": "Er is geen USB-apparaat aangesloten.",
|
||||
"usbErrorOpenFailed": "Kon het geselecteerde USB-apparaat niet openen.",
|
||||
"usbErrorConnectFailed": "Kon niet verbinding maken met het geselecteerde USB-apparaat.",
|
||||
"usbErrorUnsupported": "USB-serieel is niet ondersteund op deze platform.",
|
||||
"usbErrorAlreadyActive": "Een USB-verbinding is al actief.",
|
||||
"usbErrorNoDeviceSelected": "Geen USB-apparaat is geselecteerd.",
|
||||
"usbErrorPortClosed": "De USB-verbinding is niet actief.",
|
||||
"usbFallbackDeviceName": "Web-serieapparaat",
|
||||
"@usbConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"usbConnectionFailed": "Fout bij de USB-verbinding: {error}",
|
||||
"usbStatus_notConnected": "Selecteer een USB-apparaat",
|
||||
"usbStatus_connecting": "Verbinding maken met USB-apparaat...",
|
||||
"usbStatus_searching": "Zoeken naar USB-apparaten...",
|
||||
"usbErrorConnectTimedOut": "Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft.",
|
||||
"@tcpStatus_connectingTo": {
|
||||
"placeholders": {
|
||||
"endpoint": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@tcpConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tcpScreenTitle": "Verbind via TCP",
|
||||
"tcpHostLabel": "IP-adres",
|
||||
"tcpHostHint": "192.168.40.10",
|
||||
"connectionChoiceTcpLabel": "TCP",
|
||||
"tcpPortLabel": "Poort",
|
||||
"tcpPortHint": "5000",
|
||||
"tcpStatus_notConnected": "Voer het eindpunt in en verbind",
|
||||
"tcpStatus_connectingTo": "Verbinding maken met {endpoint}...",
|
||||
"tcpErrorHostRequired": "Een IP-adres is vereist.",
|
||||
"tcpErrorPortInvalid": "De poortwaarde moet tussen 1 en 65535 liggen.",
|
||||
"tcpErrorUnsupported": "TCP-transport wordt niet ondersteund op deze platform.",
|
||||
"tcpErrorTimedOut": "De TCP-verbinding is verlopen.",
|
||||
"tcpConnectionFailed": "Verbinding met TCP mislukt: {error}",
|
||||
"map_showDiscoveryContacts": "Ontdek contacten weergeven",
|
||||
"map_setAsMyLocation": "Stel dit in als mijn locatie",
|
||||
"@path_routeWeight": {
|
||||
"placeholders": {
|
||||
"weight": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"appSettings_initialRouteWeightSubtitle": "Startgewicht voor nieuwe, ontdekte routes",
|
||||
"appSettings_routeWeightSuccessIncrement": "Toename in het gewicht van het succes",
|
||||
"appSettings_routeWeightSuccessIncrementSubtitle": "Gewicht wordt toegevoegd aan een route na een succesvolle levering.",
|
||||
"appSettings_routeWeightFailureDecrement": "Vermindering van het gewicht van fouten",
|
||||
"appSettings_routeWeightFailureDecrementSubtitle": "Gewicht verwijderd van een pad na een mislukte levering",
|
||||
"appSettings_maxMessageRetries": "Aantal pogingen om berichten te versturen",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Aantal pogingen om een bericht opnieuw te versturen voordat het als mislukt wordt gemarkeerd",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@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",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_enableSubtitle": "Vertaal inkomende berichten en maak het mogelijk om berichten vooraf te vertalen.",
|
||||
"translation_enableTitle": "Activeer vertaling",
|
||||
"translation_title": "Vertaling",
|
||||
"translation_composerTitle": "Vertaal voor verzending",
|
||||
"translation_composerSubtitle": "Stelt de standaardstatus van het pictogram voor de vertaling van de componist in.",
|
||||
"translation_useAppLanguage": "Gebruik de taal van de app",
|
||||
"translation_targetLanguage": "Doeltaal",
|
||||
"translation_downloadedModelLabel": "Gedownloade model",
|
||||
"translation_presetModelLabel": "Voorgeprogrammeerd Hugging Face-model",
|
||||
"translation_manualUrlLabel": "URL van de handleiding",
|
||||
"translation_downloadModel": "Download het model",
|
||||
"translation_downloading": "Downloaden...",
|
||||
"translation_working": "Werken...",
|
||||
"translation_mergingChunks": "Het samenvoegen van de gedownloade stukken tot één eindbestand...",
|
||||
"translation_stop": "Stoppen",
|
||||
"translation_downloadedModels": "Gedownloade modellen",
|
||||
"translation_deleteModel": "Model verwijderen",
|
||||
"translation_modelDownloaded": "Vertalingmodel gedownload.",
|
||||
"translation_downloadStopped": "Download is afgebroken.",
|
||||
"translation_downloadFailed": "Download mislukt: {error}",
|
||||
"translation_enterUrlFirst": "Voer eerst een URL van een model in.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerDisabledHint": "Stuur berichten in de oorspronkelijke, getypte taal.",
|
||||
"translation_translateBeforeSending": "Vertaal voor verzending",
|
||||
"translation_composerEnabledHint": "De berichten worden vertaald voordat ze verzonden worden.",
|
||||
"translation_messageTranslation": "Berichtvertaling",
|
||||
"translation_translationOptions": "Opties voor vertaling",
|
||||
"translation_systemLanguage": "Taal van het systeem",
|
||||
"translation_translateTo": "Vertalen naar {language}",
|
||||
"scanner_linuxPairingShowPin": "Toon PIN",
|
||||
"scanner_linuxPairingHidePin": "PIN verbergen",
|
||||
"scanner_linuxPairingPinPrompt": "Voer PIN in voor {deviceName} (laat leeg als er geen is).",
|
||||
"scanner_linuxPairingPinTitle": "Bluetooth‑koppelings‑PIN",
|
||||
"repeater_cliQuickDiscovery": "Ontdek Buren",
|
||||
"repeater_cliQuickClockSync": "Kloksynchronisatie",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Automatisch een \"klok synchroniseren\" bericht versturen na een succesvolle inlog.",
|
||||
"repeater_clockSyncAfterLogin": "Na het inloggen, klok synchroniseren",
|
||||
"repeater_guestTools": "Gastenfuncties",
|
||||
"room_guest": "Informatie over de server",
|
||||
"chat_sendMessage": "Verzend bericht",
|
||||
"repeater_guest": "Informatie over herhalingsapparatuur"
|
||||
}
|
||||
|
||||
+528
-216
File diff suppressed because it is too large
Load Diff
+275
-1
@@ -285,6 +285,7 @@
|
||||
"contacts_newGroup": "Novo Grupo",
|
||||
"contacts_groupName": "Nome do grupo",
|
||||
"contacts_groupNameRequired": "O nome do grupo é obrigatório.",
|
||||
"contacts_groupNameReserved": "Este nome de grupo está reservado",
|
||||
"contacts_groupAlreadyExists": "O grupo \"{name}\" já existe",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -1607,6 +1608,8 @@
|
||||
"scanner_enableBluetooth": "Ative o Bluetooth",
|
||||
"scanner_bluetoothOff": "Bluetooth está desativado",
|
||||
"scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos.",
|
||||
"scanner_chromeRequired": "Navegador Chrome necessário",
|
||||
"scanner_chromeRequiredMessage": "Esta aplicação web requer o Google Chrome ou um navegador baseado no Chromium para suporte de Bluetooth.",
|
||||
"snrIndicator_nearByRepeaters": "Repetidores Próximos",
|
||||
"snrIndicator_lastSeen": "Visto pela última vez",
|
||||
"chat_ShowAllPaths": "Mostrar todos os caminhos",
|
||||
@@ -1800,5 +1803,276 @@
|
||||
"contacts_searchUsers": "Pesquisar {number}{str} Usuários...",
|
||||
"contacts_searchContactsNoNumber": "Pesquisar Contatos...",
|
||||
"contacts_unread": "Não lido",
|
||||
"contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala..."
|
||||
"contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala...",
|
||||
"settings_contactSettings": "Configurações de Contato",
|
||||
"contactsSettings_otherTitle": "Outras configurações relacionadas a contatos",
|
||||
"contactsSettings_title": "Configurações de contatos",
|
||||
"contactsSettings_autoAddTitle": "Descoberta Automática",
|
||||
"settings_contactSettingsSubtitle": "Configurações para como os contatos são adicionados",
|
||||
"contactsSettings_autoAddUsersTitle": "Adicionar usuários automaticamente",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "Permitir que o companheiro adicione automaticamente os repetidores descobertos.",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Adicionar automaticamente servidores de sala",
|
||||
"contactsSettings_overwriteOldestTitle": "Sobrescrever o Mais Antigo",
|
||||
"contactsSettings_autoAddSensorsTitle": "Adicionar sensores automaticamente",
|
||||
"discoveredContacts_Title": "Contatos Descobertos",
|
||||
"contactsSettings_autoAddUsersSubtitle": "Permitir que o companheiro adicione automaticamente os usuários descobertos.",
|
||||
"contactsSettings_autoAddRepeatersTitle": "Adicionar repetidores automaticamente",
|
||||
"discoveredContacts_noMatching": "Nenhum contato correspondente",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Permitir que o companheiro adicione automaticamente os servidores de salas descobertos.",
|
||||
"discoveredContacts_searchHint": "Pesquisar contatos descobertos",
|
||||
"contactsSettings_autoAddSensorsSubtitle": "Permitir que o companheiro adicione automaticamente sensores descobertos.",
|
||||
"discoveredContacts_copyContact": "Copiar Contato para a área de transferência",
|
||||
"discoveredContacts_deleteContact": "Excluir Contato",
|
||||
"discoveredContacts_contactAdded": "Contato adicionado",
|
||||
"discoveredContacts_addContact": "Adicionar Contato",
|
||||
"contactsSettings_overwriteOldestSubtitle": "Quando a lista de contatos estiver cheia, o contato mais antigo não favoritado será substituído.",
|
||||
"common_deleteAll": "Excluir Tudo",
|
||||
"discoveredContacts_deleteContactAll": "Excluir Todos os Contatos Descobertos",
|
||||
"discoveredContacts_deleteContactAllContent": "Tem certeza de que deseja excluir todos os contatos descobertos?",
|
||||
"map_guessedLocation": "Localização estimada",
|
||||
"map_showGuessedLocations": "Mostrar as localizações dos nós estimados",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"usbScreenTitle": "Conecte via USB",
|
||||
"usbScreenSubtitle": "Selecione um dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.",
|
||||
"usbScreenStatus": "Selecione um dispositivo USB",
|
||||
"usbScreenNote": "A comunicação serial via USB está ativa em dispositivos Android e plataformas de desktop compatíveis.",
|
||||
"usbScreenEmptyState": "Nenhum dispositivo USB encontrado. Conecte um e atualize.",
|
||||
"usbErrorPermissionDenied": "A permissão para acesso via USB foi negada.",
|
||||
"usbErrorDeviceMissing": "O dispositivo USB selecionado não está mais disponível.",
|
||||
"usbErrorInvalidPort": "Selecione um dispositivo USB válido.",
|
||||
"usbErrorBusy": "Já existe uma solicitação de conexão USB em andamento.",
|
||||
"usbErrorNotConnected": "Não há nenhum dispositivo USB conectado.",
|
||||
"usbErrorOpenFailed": "Não foi possível abrir o dispositivo USB selecionado.",
|
||||
"usbErrorConnectFailed": "Não foi possível conectar ao dispositivo USB selecionado.",
|
||||
"usbErrorUnsupported": "A comunicação serial via USB não é suportada nesta plataforma.",
|
||||
"usbErrorAlreadyActive": "A conexão USB já está ativa.",
|
||||
"usbErrorNoDeviceSelected": "Nenhum dispositivo USB foi selecionado.",
|
||||
"usbErrorPortClosed": "A conexão USB não está ativa.",
|
||||
"usbFallbackDeviceName": "Dispositivo de Serial para a Web",
|
||||
"@usbConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"usbStatus_searching": "Procurando por dispositivos USB...",
|
||||
"usbStatus_notConnected": "Selecione um dispositivo USB",
|
||||
"usbConnectionFailed": "Falha na conexão USB: {error}",
|
||||
"usbStatus_connecting": "Conectando ao dispositivo USB...",
|
||||
"usbErrorConnectTimedOut": "A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion.",
|
||||
"@tcpStatus_connectingTo": {
|
||||
"placeholders": {
|
||||
"endpoint": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@tcpConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tcpHostLabel": "Endereço IP",
|
||||
"connectionChoiceTcpLabel": "TCP",
|
||||
"tcpScreenTitle": "Estabelecer conexão via TCP",
|
||||
"tcpHostHint": "192.168.40.10",
|
||||
"tcpPortLabel": "Porta",
|
||||
"tcpPortHint": "5000",
|
||||
"tcpStatus_notConnected": "Insira o endereço final e conecte-se.",
|
||||
"tcpStatus_connectingTo": "Conectando a {endpoint}...",
|
||||
"tcpErrorHostRequired": "É necessário fornecer um endereço IP.",
|
||||
"tcpErrorPortInvalid": "O valor do porto deve estar entre 1 e 65535.",
|
||||
"tcpErrorUnsupported": "O protocolo TCP não é suportado nesta plataforma.",
|
||||
"tcpErrorTimedOut": "A conexão TCP expirou.",
|
||||
"tcpConnectionFailed": "Falha na conexão TCP: {error}",
|
||||
"map_showDiscoveryContacts": "Mostrar Contatos de Descoberta",
|
||||
"map_setAsMyLocation": "Defina minha localização",
|
||||
"@path_routeWeight": {
|
||||
"placeholders": {
|
||||
"weight": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
"appSettings_initialRouteWeightSubtitle": "Peso inicial para novos caminhos descobertos",
|
||||
"appSettings_routeWeightSuccessIncrement": "Aumento do peso para indicar sucesso",
|
||||
"appSettings_routeWeightSuccessIncrementSubtitle": "Peso adicionado a um caminho após a entrega bem-sucedida.",
|
||||
"appSettings_routeWeightFailureDecrement": "Redução do peso da falha",
|
||||
"appSettings_routeWeightFailureDecrementSubtitle": "Peso removido de um caminho após uma tentativa de entrega malsucedida.",
|
||||
"appSettings_maxMessageRetries": "Número máximo de tentativas de envio de mensagens",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Número de tentativas de reenvio antes de classificar uma mensagem como falha.",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@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",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerTitle": "Traduza antes de enviar",
|
||||
"translation_enableSubtitle": "Traduzir mensagens recebidas e permitir a tradução antes do envio.",
|
||||
"translation_enableTitle": "Ativar a tradução",
|
||||
"translation_title": "Tradução",
|
||||
"translation_composerSubtitle": "Controla o estado padrão do ícone de tradução do compositor.",
|
||||
"translation_targetLanguage": "Língua-alvo",
|
||||
"translation_useAppLanguage": "Utilize o idioma da aplicação",
|
||||
"translation_downloadedModelLabel": "Modelo baixado",
|
||||
"translation_presetModelLabel": "Modelo pré-definido da Hugging Face",
|
||||
"translation_manualUrlLabel": "URL do modelo manual",
|
||||
"translation_downloading": "Baixando...",
|
||||
"translation_downloadModel": "Baixar modelo",
|
||||
"translation_working": "Trabalhando...",
|
||||
"translation_stop": "Pare",
|
||||
"translation_mergingChunks": "Combinando os fragmentos baixados em um único arquivo...",
|
||||
"translation_downloadedModels": "Modelos baixados",
|
||||
"translation_deleteModel": "Excluir modelo",
|
||||
"translation_modelDownloaded": "Modelo de tradução baixado.",
|
||||
"translation_downloadStopped": "Download interrompido.",
|
||||
"translation_downloadFailed": "Falha na descarga: {error}",
|
||||
"translation_enterUrlFirst": "Insira primeiro a URL do modelo.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_messageTranslation": "Tradução da mensagem",
|
||||
"translation_translateBeforeSending": "Traduzir antes de enviar",
|
||||
"translation_composerEnabledHint": "As mensagens serão traduzidas antes de serem enviadas.",
|
||||
"translation_composerDisabledHint": "Envie mensagens no idioma original, conforme digitado.",
|
||||
"translation_translateTo": "Traduzir para {language}",
|
||||
"translation_translationOptions": "Opções de tradução",
|
||||
"translation_systemLanguage": "Idioma do sistema",
|
||||
"scanner_linuxPairingShowPin": "Mostrar PIN",
|
||||
"scanner_linuxPairingHidePin": "Ocultar PIN",
|
||||
"scanner_linuxPairingPinPrompt": "Insira o PIN para {deviceName} (deixe em branco se não houver).",
|
||||
"scanner_linuxPairingPinTitle": "PIN de emparelhamento Bluetooth",
|
||||
"repeater_cliQuickClockSync": "Sincronização do Relógio",
|
||||
"repeater_cliQuickDiscovery": "Descobrir Vizinhos",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Enviar automaticamente a sincronização do \"relógio\" após um login bem-sucedido.",
|
||||
"repeater_clockSyncAfterLogin": "Sincronização do relógio após o login",
|
||||
"room_guest": "Informações do Servidor",
|
||||
"chat_sendMessage": "Enviar mensagem",
|
||||
"repeater_guest": "Informações sobre repetidores",
|
||||
"repeater_guestTools": "Ferramentas para hóspedes"
|
||||
}
|
||||
|
||||
+275
-1
@@ -212,6 +212,7 @@
|
||||
"contacts_newGroup": "Новая группа",
|
||||
"contacts_groupName": "Имя группы",
|
||||
"contacts_groupNameRequired": "Имя группы обязательно",
|
||||
"contacts_groupNameReserved": "Это имя группы зарезервировано",
|
||||
"contacts_groupAlreadyExists": "Группа \"{name}\" уже существует",
|
||||
"contacts_filterContacts": "Фильтр контактов...",
|
||||
"contacts_noContactsMatchFilter": "Нет контактов, соответствующих фильтру",
|
||||
@@ -847,6 +848,8 @@
|
||||
"scanner_enableBluetooth": "Включите Bluetooth",
|
||||
"scanner_bluetoothOff": "Bluetooth выключен",
|
||||
"scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства.",
|
||||
"scanner_chromeRequired": "Требуется браузер Chrome",
|
||||
"scanner_chromeRequiredMessage": "Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.",
|
||||
"snrIndicator_nearByRepeaters": "Ближайшие ретрансляторы",
|
||||
"snrIndicator_lastSeen": "Последний раз видели",
|
||||
"chat_ShowAllPaths": "Показать все пути",
|
||||
@@ -1040,5 +1043,276 @@
|
||||
"contacts_unread": "Непрочитанное",
|
||||
"contacts_searchRoomServers": "Поиск {number}{str} серверов комнат...",
|
||||
"contacts_searchFavorites": "Поиск {number}{str} избранного...",
|
||||
"contacts_searchUsers": "Поиск {number}{str} пользователей..."
|
||||
"contacts_searchUsers": "Поиск {number}{str} пользователей...",
|
||||
"settings_contactSettings": "Настройки контактов",
|
||||
"settings_contactSettingsSubtitle": "Настройки добавления контактов",
|
||||
"contactsSettings_autoAddTitle": "Автоматическое обнаружение",
|
||||
"contactsSettings_title": "Настройки контактов",
|
||||
"contactsSettings_otherTitle": "Другие настройки, связанные с контактами",
|
||||
"contactsSettings_autoAddUsersSubtitle": "Разрешить компаньону автоматически добавлять обнаруженных пользователей",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Автоматически добавлять серверы комнат",
|
||||
"contactsSettings_autoAddSensorsTitle": "Автоматически добавлять датчики",
|
||||
"contactsSettings_autoAddSensorsSubtitle": "Разрешить компаньону автоматически добавлять обнаруженные датчики",
|
||||
"contactsSettings_autoAddUsersTitle": "Автоматически добавлять пользователей",
|
||||
"contactsSettings_overwriteOldestTitle": "Перезаписать самое старое",
|
||||
"contactsSettings_autoAddRepeatersTitle": "Автоматически добавлять ретрансляторы",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "Разрешить спутнику автоматически добавлять обнаруженные ретрансляторы",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Разрешить компаньону автоматически добавлять обнаруженные сервера комнат.",
|
||||
"discoveredContacts_noMatching": "Нет совпадающих контактов",
|
||||
"discoveredContacts_searchHint": "Найденные контакты поиска",
|
||||
"discoveredContacts_contactAdded": "Контакт добавлен",
|
||||
"discoveredContacts_copyContact": "Копировать контакт в буфер обмена",
|
||||
"discoveredContacts_addContact": "Добавить контакт",
|
||||
"discoveredContacts_Title": "Обнаруженные контакты",
|
||||
"discoveredContacts_deleteContact": "Удалить контакт",
|
||||
"contactsSettings_overwriteOldestSubtitle": "Когда список контактов заполнен, будет заменен самый старый контакт, который не находится в избранном.",
|
||||
"common_deleteAll": "Удалить все",
|
||||
"discoveredContacts_deleteContactAllContent": "Вы уверены, что хотите удалить все обнаруженные контакты?",
|
||||
"discoveredContacts_deleteContactAll": "Удалить Все Обнаруженные Контакты",
|
||||
"map_guessedLocation": "Угаданное место",
|
||||
"map_showGuessedLocations": "Отобразить предполагаемые места расположения узлов",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"usbScreenSubtitle": "Выберите обнаруженное устройство с последовательным интерфейсом и подключите его напрямую к вашему узлу MeshCore.",
|
||||
"usbScreenTitle": "Подключение через USB",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"usbScreenStatus": "Выберите USB-устройство",
|
||||
"usbScreenNote": "USB-серийный порт активен на поддерживаемых устройствах Android и на настольных платформах.",
|
||||
"usbScreenEmptyState": "Не обнаружено устройств USB. Подключите одно из них и обновите список.",
|
||||
"usbErrorPermissionDenied": "Запрос на доступ через USB был отклонен.",
|
||||
"usbErrorDeviceMissing": "Выбранное USB-устройство больше недоступно.",
|
||||
"usbErrorInvalidPort": "Выберите действительное USB-устройство.",
|
||||
"usbErrorBusy": "Еще одно запрошенное соединение через USB уже находится в процессе.",
|
||||
"usbErrorNotConnected": "Ни одно USB-устройство не подключено.",
|
||||
"usbErrorOpenFailed": "Не удалось открыть выбранное USB-устройство.",
|
||||
"usbErrorConnectFailed": "Не удалось установить соединение с выбранным USB-устройством.",
|
||||
"usbErrorUnsupported": "Поддержка последовательного USB отсутствует на данной платформе.",
|
||||
"usbErrorAlreadyActive": "USB-соединение уже установлено.",
|
||||
"usbErrorNoDeviceSelected": "Не было выбрано ни одно устройство USB.",
|
||||
"usbErrorPortClosed": "USB-соединение не установлено.",
|
||||
"usbFallbackDeviceName": "Устройство для последовательного подключения к сети",
|
||||
"@usbConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"usbStatus_searching": "Поиск USB-устройств...",
|
||||
"usbStatus_connecting": "Подключение к USB-устройству...",
|
||||
"usbConnectionFailed": "Не удалось установить соединение через USB: {error}",
|
||||
"usbStatus_notConnected": "Выберите USB-устройство",
|
||||
"usbErrorConnectTimedOut": "Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion.",
|
||||
"@tcpStatus_connectingTo": {
|
||||
"placeholders": {
|
||||
"endpoint": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@tcpConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tcpHostHint": "192.168.40.10",
|
||||
"connectionChoiceTcpLabel": "TCP",
|
||||
"tcpHostLabel": "IP-адрес",
|
||||
"tcpScreenTitle": "Установить соединение по протоколу TCP",
|
||||
"tcpPortLabel": "Порт",
|
||||
"tcpPortHint": "5000",
|
||||
"tcpStatus_notConnected": "Введите адрес и подключитесь.",
|
||||
"tcpStatus_connectingTo": "Подключение к {endpoint}...",
|
||||
"tcpErrorHostRequired": "Необходимо указать IP-адрес.",
|
||||
"tcpErrorPortInvalid": "Порт должен находиться в диапазоне от 1 до 65535.",
|
||||
"tcpErrorUnsupported": "Протокол TCP не поддерживается на этой платформе.",
|
||||
"tcpErrorTimedOut": "Соединение TCP не удалось установить.",
|
||||
"tcpConnectionFailed": "Не удалось установить соединение TCP: {error}",
|
||||
"map_showDiscoveryContacts": "Показать контакты Discovery",
|
||||
"map_setAsMyLocation": "Установить мое местоположение",
|
||||
"@path_routeWeight": {
|
||||
"placeholders": {
|
||||
"weight": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": "Начальный вес для новых, только что открытых маршрутов",
|
||||
"appSettings_initialRouteWeight": "Начальный вес маршрута",
|
||||
"appSettings_routeWeightSuccessIncrement": "Увеличение веса успеха",
|
||||
"appSettings_routeWeightSuccessIncrementSubtitle": "Вес, добавленный к маршруту после успешной доставки.",
|
||||
"appSettings_routeWeightFailureDecrement": "Уменьшение веса неудачи",
|
||||
"appSettings_routeWeightFailureDecrementSubtitle": "Вес, который был удален с пути после неудачной доставки.",
|
||||
"appSettings_maxMessageRetries": "Максимальное количество повторных попыток отправки сообщения",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@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 и время передачи",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_enableSubtitle": "Переводить входящие сообщения и позволять предварительный перевод перед отправкой.",
|
||||
"translation_composerTitle": "Переводить перед отправкой",
|
||||
"translation_title": "Перевод",
|
||||
"translation_enableTitle": "Включить перевод",
|
||||
"translation_composerSubtitle": "Управляет исходным состоянием значка перевода, предоставляемого редактором.",
|
||||
"translation_targetLanguage": "Целевой язык",
|
||||
"translation_useAppLanguage": "Используйте язык приложения",
|
||||
"translation_downloadedModelLabel": "Загруженная модель",
|
||||
"translation_presetModelLabel": "Предопределенная модель от Hugging Face",
|
||||
"translation_manualUrlLabel": "Ссылка на руководство",
|
||||
"translation_downloadModel": "Скачать модель",
|
||||
"translation_downloading": "Загрузка...",
|
||||
"translation_stop": "Прекратите",
|
||||
"translation_working": "Работа...",
|
||||
"translation_mergingChunks": "Объединение скачанных фрагментов в один финальный файл...",
|
||||
"translation_downloadedModels": "Загруженные модели",
|
||||
"translation_deleteModel": "Удалить модель",
|
||||
"translation_modelDownloaded": "Модель перевода загружена.",
|
||||
"translation_downloadStopped": "Процесс загрузки был прерван.",
|
||||
"translation_downloadFailed": "Не удалось скачать: {error}",
|
||||
"translation_enterUrlFirst": "Сначала введите URL модели.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_translateBeforeSending": "Перевести перед отправкой",
|
||||
"translation_composerEnabledHint": "Сообщения будут переведены перед отправкой.",
|
||||
"translation_messageTranslation": "Перевод сообщения",
|
||||
"translation_composerDisabledHint": "Отправляйте сообщения на языке, в котором они были изначально набраны.",
|
||||
"translation_translateTo": "Перевести на {language}",
|
||||
"translation_translationOptions": "Варианты перевода",
|
||||
"translation_systemLanguage": "Язык системы",
|
||||
"scanner_linuxPairingShowPin": "Показать PIN",
|
||||
"scanner_linuxPairingPinPrompt": "Введите PIN‑код для {deviceName} (оставьте пустым, если нет).",
|
||||
"scanner_linuxPairingHidePin": "Скрыть PIN",
|
||||
"scanner_linuxPairingPinTitle": "PIN‑код сопряжения Bluetooth",
|
||||
"repeater_cliQuickDiscovery": "Обнаружить Соседей",
|
||||
"repeater_cliQuickClockSync": "Синхронизация часов",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLogin": "Синхронизация часов после входа в систему",
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Автоматически отправлять сообщение \"синхронизация времени\" после успешной авторизации.",
|
||||
"chat_sendMessage": "Отправить сообщение",
|
||||
"repeater_guest": "Информация о ретрансляторе",
|
||||
"room_guest": "Информация о сервере",
|
||||
"repeater_guestTools": "Инструменты для гостей"
|
||||
}
|
||||
|
||||
+275
-1
@@ -285,6 +285,7 @@
|
||||
"contacts_newGroup": "Nová skupina",
|
||||
"contacts_groupName": "Názov skupiny",
|
||||
"contacts_groupNameRequired": "Skupina musí mať názov.",
|
||||
"contacts_groupNameReserved": "Tento názov skupiny je rezervovaný",
|
||||
"contacts_groupAlreadyExists": "Skupina \"{name}\" už existuje",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -1605,6 +1606,8 @@
|
||||
"map_runTrace": "Spustiť trasovaním cesty",
|
||||
"map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené.",
|
||||
"scanner_bluetoothOffMessage": "Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.",
|
||||
"scanner_chromeRequired": "Vyžaduje sa prehliadač Chrome",
|
||||
"scanner_chromeRequiredMessage": "Táto webová aplikácia vyžaduje Google Chrome alebo prehliadač založený na Chromium pre podporu Bluetooth.",
|
||||
"scanner_bluetoothOff": "Bluetooth je vypnutý",
|
||||
"scanner_enableBluetooth": "Povolte Bluetooth",
|
||||
"snrIndicator_lastSeen": "Naposledy videný",
|
||||
@@ -1800,5 +1803,276 @@
|
||||
"contacts_searchRepeaters": "Hľadať {number}{str} opakovače...",
|
||||
"contacts_searchUsers": "Hľadať {number}{str} používateľov...",
|
||||
"contacts_searchContactsNoNumber": "Hľadať kontakty...",
|
||||
"contacts_unread": "Neprečítané"
|
||||
"contacts_unread": "Neprečítané",
|
||||
"settings_contactSettingsSubtitle": "Nastavenia pre pridávanie kontaktov.",
|
||||
"contactsSettings_autoAddUsersTitle": "Automaticky pridávať užívateľov",
|
||||
"contactsSettings_autoAddUsersSubtitle": "Povoliť spoločníkovi automaticky pridávať objavených užívateľov.",
|
||||
"contactsSettings_autoAddRepeatersTitle": "Automaticky pridávať opakovače",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Automaticky pridávať server miestnosti",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Povoliť spoločníkovi automaticky pridať objavené serverové miestnosti.",
|
||||
"contactsSettings_autoAddTitle": "Automatické zisťovanie",
|
||||
"contactsSettings_title": "Nastavenia kontaktov",
|
||||
"contactsSettings_otherTitle": "Ďalšie nastavenia súvisiace s kontaktami",
|
||||
"settings_contactSettings": "Nastavenia kontaktov",
|
||||
"contactsSettings_autoAddSensorsTitle": "Automaticky pridávať senzory",
|
||||
"discoveredContacts_noMatching": "Žiadne zhodné kontakty",
|
||||
"discoveredContacts_searchHint": "Vyhľadať objavené kontakty",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "Povoliť spoločníkovi automaticky pridávať objavené repeater.",
|
||||
"discoveredContacts_contactAdded": "Kontakt bol pridaný",
|
||||
"discoveredContacts_copyContact": "Kopírovať kontakt do schránky",
|
||||
"discoveredContacts_deleteContact": "Zmazať kontakt",
|
||||
"contactsSettings_autoAddSensorsSubtitle": "Povoliť spoločníkovi automaticky pridávať objavené senzory.",
|
||||
"discoveredContacts_Title": "Objavené kontakty",
|
||||
"contactsSettings_overwriteOldestTitle": "Prepísať najstaršie",
|
||||
"discoveredContacts_addContact": "Pridať kontakt",
|
||||
"contactsSettings_overwriteOldestSubtitle": "Keď je zoznam kontaktov plný, bude nahradený najstarší neoznačený kontakt.",
|
||||
"discoveredContacts_deleteContactAll": "Zmazať všetky objavené kontakty",
|
||||
"common_deleteAll": "Zmazať všetko",
|
||||
"discoveredContacts_deleteContactAllContent": "Ste si istí, že chcete zmazať všetky objavené kontakty?",
|
||||
"map_showGuessedLocations": "Zobraziť umiestnenia odhadnutých uzlov",
|
||||
"map_guessedLocation": "Odhadnutá lokalita",
|
||||
"usbScreenTitle": "Pripojte cez USB",
|
||||
"usbScreenSubtitle": "Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vašej MeshCore uzlu.",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"usbScreenStatus": "Vyberte USB zariadenie",
|
||||
"usbScreenNote": "USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.",
|
||||
"usbScreenEmptyState": "Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte.",
|
||||
"usbErrorPermissionDenied": "Žiadosť o prístup cez USB bola zamietnutá.",
|
||||
"usbErrorDeviceMissing": "Vybrané USB zariadenie už nie je dostupné.",
|
||||
"usbErrorInvalidPort": "Vyberte platné USB zariadenie.",
|
||||
"usbErrorBusy": "Ďalšia požiadavka na pripojenie cez USB je aktuálne v procese.",
|
||||
"usbErrorNotConnected": "Nie je pripojené žiadne USB zariadenie.",
|
||||
"usbErrorOpenFailed": "Nepodarilo sa otvoriť vybrané USB zariadenie.",
|
||||
"usbErrorConnectFailed": "Nepodarilo sa sa sa pripojiť k vybranému USB zariadeniu.",
|
||||
"usbErrorUnsupported": "Podpora USB sériového rozhrania nie je na tejto platforme dostupná.",
|
||||
"usbErrorAlreadyActive": "Pripojenie cez USB je už aktivované.",
|
||||
"usbErrorNoDeviceSelected": "Nebolo vybrané žiadne USB zariadenie.",
|
||||
"usbErrorPortClosed": "Pripojenie cez USB nie je aktivované.",
|
||||
"usbFallbackDeviceName": "Webový sériový zariadenie",
|
||||
"@usbConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"usbStatus_searching": "Hľadanie USB zariadení...",
|
||||
"usbConnectionFailed": "Neúspešné pripojenie cez USB: {error}",
|
||||
"usbStatus_notConnected": "Vyberte USB zariadenie",
|
||||
"usbStatus_connecting": "Pripojenie k USB zariadeniu...",
|
||||
"usbErrorConnectTimedOut": "Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion.",
|
||||
"@tcpStatus_connectingTo": {
|
||||
"placeholders": {
|
||||
"endpoint": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@tcpConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tcpHostHint": "192.168.40.10",
|
||||
"tcpHostLabel": "IP adresa",
|
||||
"tcpScreenTitle": "Spojte sa pomocou protokolu TCP",
|
||||
"connectionChoiceTcpLabel": "TCP",
|
||||
"tcpPortLabel": "Port",
|
||||
"tcpPortHint": "5000",
|
||||
"tcpStatus_notConnected": "Zadajte cieľovú adresu a pripojte sa.",
|
||||
"tcpStatus_connectingTo": "Pripojenie k {endpoint}...",
|
||||
"tcpErrorHostRequired": "Je potrebné zadať IP adresu.",
|
||||
"tcpErrorPortInvalid": "Číslo portu musí byť medzi 1 a 65535.",
|
||||
"tcpErrorUnsupported": "Prevoz prostredníctvom protokolu TCP nie je na tejto platforme podporovaný.",
|
||||
"tcpErrorTimedOut": "Pripojenie TCP vypršalo.",
|
||||
"tcpConnectionFailed": "Neúspešné vytvorenie TCP spojenia: {error}",
|
||||
"map_showDiscoveryContacts": "Zobraziť kontakty objavov",
|
||||
"map_setAsMyLocation": "Nastavte ako moju polohu",
|
||||
"@path_routeWeight": {
|
||||
"placeholders": {
|
||||
"weight": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"appSettings_maxRouteWeight": "Maximálna hmotnosť trasy",
|
||||
"appSettings_routeWeightSuccessIncrement": "Zvyšenie váhy úspechu",
|
||||
"appSettings_routeWeightSuccessIncrementSubtitle": "Hmotnosť pridaná k trase po úspešnej doručení",
|
||||
"appSettings_routeWeightFailureDecrement": "Sníženie váhy, ktorá sa používa na odhad rizika.",
|
||||
"appSettings_routeWeightFailureDecrementSubtitle": "Hmotnosť odstránená z cesty po neúspešnej doručenie",
|
||||
"appSettings_maxMessageRetries": "Maximalný počet pokusov o doručenie správ",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Počet pokusov o odošleť pred označením správy ako neúspešnej",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@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",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_enableSubtitle": "Prekladajte prichádzajúce správy a umožnite ich preklad pred odoslaním.",
|
||||
"translation_enableTitle": "Aktivovať preklad",
|
||||
"translation_composerTitle": "Preložte pred odeslaním",
|
||||
"translation_title": "Preklad",
|
||||
"translation_composerSubtitle": "Riadi výchoce stav ikony pre preklad, ktorú používa program.",
|
||||
"translation_targetLanguage": "Cieľový jazyk",
|
||||
"translation_useAppLanguage": "Použite jazyk aplikácie",
|
||||
"translation_downloadedModelLabel": "Stiahnutý model",
|
||||
"translation_presetModelLabel": "Prednastavený model od Hugging Face",
|
||||
"translation_manualUrlLabel": "Odkaz na manuál (v elektronickej forme)",
|
||||
"translation_downloadModel": "Stiahnuť model",
|
||||
"translation_downloading": "Stiahnutie...",
|
||||
"translation_working": "Práca...",
|
||||
"translation_stop": "Zastavte",
|
||||
"translation_mergingChunks": "Sliečenie stiahnutých častí do konečného súboru...",
|
||||
"translation_downloadedModels": "Stiahnuté modely",
|
||||
"translation_deleteModel": "Odstrániť model",
|
||||
"translation_modelDownloaded": "Model pre preklad bol stiahnutý.",
|
||||
"translation_downloadStopped": "Stiahnutie bolo prerušené.",
|
||||
"translation_downloadFailed": "Neúspešné stiahnutie: {error}",
|
||||
"translation_enterUrlFirst": "Najprv zadajte URL pre konkrétny model.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner_linuxPairingHidePin": "Skryť PIN",
|
||||
"scanner_linuxPairingShowPin": "Zobraziť PIN",
|
||||
"scanner_linuxPairingPinTitle": "PIN pre párovanie cez Bluetooth",
|
||||
"scanner_linuxPairingPinPrompt": "Zadajte PIN pre {deviceName} (ak neexistuje, nechajte prázdne).",
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerDisabledHint": "Posielajte správy v pôvodnej písanom jazyku.",
|
||||
"translation_composerEnabledHint": "Správy budú preložené, než budú odoslané.",
|
||||
"translation_translateBeforeSending": "Preložte pred odeslaním",
|
||||
"translation_messageTranslation": "Preklad textu",
|
||||
"translation_translateTo": "Preložte do {language}",
|
||||
"translation_translationOptions": "Možnosti prekladania",
|
||||
"translation_systemLanguage": "Jazyk systému",
|
||||
"repeater_cliQuickClockSync": "Synchronizácia hodin",
|
||||
"repeater_cliQuickDiscovery": "Objaviť susedov",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLogin": "Synchronizácia hodiniek po prihlávení",
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Automaticky posielajte notifikáciu \"synchronizácia času\" po úspešnom prihládení.",
|
||||
"chat_sendMessage": "Odoslať správu",
|
||||
"repeater_guest": "Informácie o opakovači",
|
||||
"room_guest": "Informácie o serveri",
|
||||
"repeater_guestTools": "Nástroje pre hostí"
|
||||
}
|
||||
|
||||
+275
-1
@@ -285,6 +285,7 @@
|
||||
"contacts_newGroup": "Nova skupina",
|
||||
"contacts_groupName": "Ime skupine",
|
||||
"contacts_groupNameRequired": "Ime skupine je obvezno.",
|
||||
"contacts_groupNameReserved": "To ime skupine je rezervirano",
|
||||
"contacts_groupAlreadyExists": "Skupina \"{name}\" že obstaja",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -1606,6 +1607,8 @@
|
||||
"map_pathTraceCancelled": "Spremljanje poti je prekinjeno.",
|
||||
"scanner_enableBluetooth": "Omogočite Bluetooth",
|
||||
"scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.",
|
||||
"scanner_chromeRequired": "Zahtevan brskalnik Chrome",
|
||||
"scanner_chromeRequiredMessage": "Ta spletna aplikacija za podporo Bluetooth zahteva Google Chrome ali brskalnik na osnovi Chromiuma.",
|
||||
"scanner_bluetoothOff": "Bluetooth je izklopljen",
|
||||
"snrIndicator_lastSeen": "Zadnjič videno",
|
||||
"snrIndicator_nearByRepeaters": "Bližnji ponovitelji",
|
||||
@@ -1800,5 +1803,276 @@
|
||||
"contacts_searchRoomServers": "Išči {number}{str} strežnikov sob...",
|
||||
"contacts_searchContactsNoNumber": "Iskanje stikov...",
|
||||
"contacts_searchRepeaters": "Išči {number}{str} ponavljalnike...",
|
||||
"contacts_searchUsers": "Išči {number}{str} uporabnikov..."
|
||||
"contacts_searchUsers": "Išči {number}{str} uporabnikov...",
|
||||
"settings_contactSettings": "Nastavitve stika",
|
||||
"contactsSettings_autoAddTitle": "Avtomatsko odkrivanje",
|
||||
"contactsSettings_autoAddUsersTitle": "Avtomatsko dodaj uporabnike",
|
||||
"contactsSettings_autoAddRepeatersTitle": "Avtomatsko dodaj ponovitelje",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite ponovitelje.",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Avtomatsko dodaj strežnike sob",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite strežnike sob.",
|
||||
"contactsSettings_otherTitle": "Druge nastavitve v zvezi s stiki",
|
||||
"settings_contactSettingsSubtitle": "Nastavitve za dodajanje stikov.",
|
||||
"contactsSettings_title": "Nastavitve stikov",
|
||||
"contactsSettings_autoAddSensorsTitle": "Avtomatsko dodaj senzorje",
|
||||
"contactsSettings_autoAddUsersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite uporabnike.",
|
||||
"discoveredContacts_noMatching": "Ni ujemajočih stikov",
|
||||
"contactsSettings_autoAddSensorsSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite senzorje.",
|
||||
"discoveredContacts_addContact": "Dodaj stik",
|
||||
"discoveredContacts_contactAdded": "Kontakt dodan",
|
||||
"discoveredContacts_copyContact": "Kopiraj stik v odložišče",
|
||||
"contactsSettings_overwriteOldestTitle": "Prepiši najstarejše",
|
||||
"discoveredContacts_Title": "Odkriti stiki",
|
||||
"discoveredContacts_searchHint": "Najdeni stiki po iskanju",
|
||||
"discoveredContacts_deleteContact": "Izbriši stik",
|
||||
"contactsSettings_overwriteOldestSubtitle": "Ko je seznam stikov poln, bo najstarejši nestarševski stik zamenjan.",
|
||||
"common_deleteAll": "Izbriši vse",
|
||||
"discoveredContacts_deleteContactAllContent": "Ste prepričani, da želite izbrisati vse odkrite kontakte?",
|
||||
"discoveredContacts_deleteContactAll": "Izbriši vse odkrite kontakte",
|
||||
"map_guessedLocation": "Predpostavljena lokacija",
|
||||
"map_showGuessedLocations": "Pokaži lokacije domnevnih not.",
|
||||
"usbScreenTitle": "Povežite preko USB",
|
||||
"usbScreenSubtitle": "Izberite zaznano serijsko napravo in se neposredno povežite z vašo MeshCore napravo.",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"usbScreenStatus": "Izberite USB naprave",
|
||||
"usbScreenNote": "USB serijska povezava je aktivna na podprtih napravah Android in na desktop platformah.",
|
||||
"usbScreenEmptyState": "Niti en USB naprave niso najdeni. Povežite eno in posodobite.",
|
||||
"usbErrorPermissionDenied": "Dovoljenje za dostop preko USB-ja je bilo zavrnjeno.",
|
||||
"usbErrorDeviceMissing": "Izbrani USB napravej je več ne.",
|
||||
"usbErrorInvalidPort": "Izberite veljavno USB naprave.",
|
||||
"usbErrorBusy": "Že je v teku zahteva za povezavo preko USB.",
|
||||
"usbErrorNotConnected": "Ni priklopljenih USB naprave.",
|
||||
"usbErrorOpenFailed": "Uspešno ni bilo mogo, da se odpre izbran naprave USB.",
|
||||
"usbErrorConnectFailed": "Niso bilo mogoče uskladiti povezave z izbranim USB napom.",
|
||||
"usbErrorUnsupported": "USB serijska komunikacija ni podprta na tej platformi.",
|
||||
"usbErrorAlreadyActive": "USB povezava je že aktivirana.",
|
||||
"usbErrorNoDeviceSelected": "Ni bilo izbranega USB naprave.",
|
||||
"usbErrorPortClosed": "USB povezava ni aktivirana.",
|
||||
"usbFallbackDeviceName": "Naprave za serijsko komunikacijo preko spleta",
|
||||
"@usbConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"usbStatus_notConnected": "Izberite USB naprave.",
|
||||
"usbStatus_connecting": "Povezava z USB napravo...",
|
||||
"usbStatus_searching": "Iskanje USB naprav...",
|
||||
"usbConnectionFailed": "Napaka pri povezavi preko USB: {error}",
|
||||
"usbErrorConnectTimedOut": "Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion.",
|
||||
"@tcpStatus_connectingTo": {
|
||||
"placeholders": {
|
||||
"endpoint": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@tcpConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"connectionChoiceTcpLabel": "TCP",
|
||||
"tcpHostLabel": "IP naslov",
|
||||
"tcpHostHint": "192.168.40.10",
|
||||
"tcpScreenTitle": "Komunicirajte preko protokola TCP",
|
||||
"tcpPortLabel": "Vrata",
|
||||
"tcpPortHint": "5000",
|
||||
"tcpStatus_notConnected": "Vnesite končni naslov in se povežite",
|
||||
"tcpStatus_connectingTo": "Povezava z {endpoint}...",
|
||||
"tcpErrorHostRequired": "Potrebna je IP-naslov.",
|
||||
"tcpErrorPortInvalid": "Port mora biti med 1 in 65535.",
|
||||
"tcpErrorUnsupported": "Transport preko protokola TCP ni podprt na tej platformi.",
|
||||
"tcpErrorTimedOut": "Povezava TCP je presegla časovno obdobje.",
|
||||
"tcpConnectionFailed": "Napaka pri povezavi TCP: {error}",
|
||||
"map_showDiscoveryContacts": "Prikaži odkritja kontaktov",
|
||||
"map_setAsMyLocation": "Nastavite to kot mojo lokacijo",
|
||||
"@path_routeWeight": {
|
||||
"placeholders": {
|
||||
"weight": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"appSettings_maxRouteWeight": "Največja dovoljena teža poti",
|
||||
"appSettings_routeWeightSuccessIncrement": "Učinkovitost: povečanje",
|
||||
"appSettings_routeWeightSuccessIncrementSubtitle": "Težava, dodana poti po uspešni dostavi",
|
||||
"appSettings_routeWeightFailureDecrement": "Zmanjšanje teže, ki je povezana s pomanjkanjem",
|
||||
"appSettings_routeWeightFailureDecrementSubtitle": "Težo, ki ni bila uspešno dostavljena, odstranili s poti.",
|
||||
"appSettings_maxMessageRetries": "Najve število poskusov pošiljanja sporočil",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@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",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerTitle": "Preprištejte, preden pošljete",
|
||||
"translation_title": "Prevod",
|
||||
"translation_enableSubtitle": "Prevedite vstopne sporočila in omogočite predhodno prevajanje.",
|
||||
"translation_enableTitle": "Omogočite prevod",
|
||||
"translation_composerSubtitle": "Ureja privzeto stanje ikone za prevod, ki jo uporablja avtor.",
|
||||
"translation_targetLanguage": "Ciljna jezika",
|
||||
"translation_useAppLanguage": "Uporabite jezik aplikacije",
|
||||
"translation_downloadedModelLabel": "Naložen model",
|
||||
"translation_presetModelLabel": "Prednastavljeni model Hugging Face",
|
||||
"translation_manualUrlLabel": "URL za ročni model",
|
||||
"translation_downloadModel": "Prenesite model",
|
||||
"translation_downloading": "Izvajanje...",
|
||||
"translation_working": "Delo...",
|
||||
"translation_stop": "Prekliji",
|
||||
"translation_mergingChunks": "Sklapljanje prenesenih delov v končni datoteko...",
|
||||
"translation_downloadedModels": "Naloženi modeli",
|
||||
"translation_deleteModel": "Izbrisati model",
|
||||
"translation_modelDownloaded": "Model za prevajanje je bil naložen.",
|
||||
"translation_downloadStopped": "Prenos je bil prekinjen.",
|
||||
"translation_downloadFailed": "Izgovoritev ni bila uspešna: {error}",
|
||||
"translation_enterUrlFirst": "Najprej vnesite URL model.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_translateBeforeSending": "Preprištejte, preden pošljete",
|
||||
"translation_composerDisabledHint": "Pošljite sporočila v originalnem tipkanem jeziku.",
|
||||
"translation_composerEnabledHint": "Vsebina sporočil bo prevedena, preden jih pošljemo.",
|
||||
"translation_messageTranslation": "Prevod sporočila",
|
||||
"translation_translateTo": "Prevesti v {language}",
|
||||
"translation_translationOptions": "Možnosti prevoda",
|
||||
"translation_systemLanguage": "Jezik sistema",
|
||||
"scanner_linuxPairingShowPin": "Prikaži PIN",
|
||||
"scanner_linuxPairingHidePin": "Skrij PIN",
|
||||
"scanner_linuxPairingPinPrompt": "Vnesite PIN za {deviceName} (pustite prazno, če ga ni).",
|
||||
"scanner_linuxPairingPinTitle": "Bluetooth PIN za seznanjanje",
|
||||
"repeater_cliQuickDiscovery": "Odkrijte sosede",
|
||||
"repeater_cliQuickClockSync": "Usklajevanje ure",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Samodejno po uspešnem vstopu pošljite obvestilo o sinhronizaciji časa.",
|
||||
"repeater_clockSyncAfterLogin": "Sinhronizacija ure po prijavi",
|
||||
"repeater_guest": "Informacije o ponovljalniku",
|
||||
"chat_sendMessage": "Pošlji sporočilo",
|
||||
"room_guest": "Informacije o strežniku",
|
||||
"repeater_guestTools": "Naložila za goste"
|
||||
}
|
||||
|
||||
+275
-1
@@ -285,6 +285,7 @@
|
||||
"contacts_newGroup": "Ny grupp",
|
||||
"contacts_groupName": "Gruppnamn",
|
||||
"contacts_groupNameRequired": "Gruppnamnet är obligatoriskt",
|
||||
"contacts_groupNameReserved": "Detta gruppnamn är reserverat",
|
||||
"contacts_groupAlreadyExists": "Gruppen \"{name}\" finns redan.",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -1606,6 +1607,8 @@
|
||||
"map_removeLast": "Ta bort sista",
|
||||
"scanner_enableBluetooth": "Aktivera Bluetooth",
|
||||
"scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.",
|
||||
"scanner_chromeRequired": "Chrome-webbläsare krävs",
|
||||
"scanner_chromeRequiredMessage": "Denna webbapplikation kräver Google Chrome oder en Chromium-baserader webbläsare för Bluetooth-stöd.",
|
||||
"scanner_bluetoothOff": "Bluetooth är avstängt",
|
||||
"snrIndicator_lastSeen": "Senast sedd",
|
||||
"snrIndicator_nearByRepeaters": "Närliggande uppreparstationer",
|
||||
@@ -1800,5 +1803,276 @@
|
||||
"contacts_searchRepeaters": "Sök {number}{str} upprepningsenheter...",
|
||||
"contacts_searchFavorites": "Sök {number}{str} Favoriter...",
|
||||
"contacts_searchUsers": "Sök {number}{str} användare...",
|
||||
"contacts_searchRoomServers": "Sök {number}{str} Room-servrar..."
|
||||
"contacts_searchRoomServers": "Sök {number}{str} Room-servrar...",
|
||||
"settings_contactSettingsSubtitle": "Inställningar för hur kontakter läggs till.",
|
||||
"settings_contactSettings": "Kontaktinställningar",
|
||||
"contactsSettings_autoAddTitle": "Automatisk upptäckt",
|
||||
"contactsSettings_otherTitle": "Andra inställningar relaterade till kontakt",
|
||||
"contactsSettings_autoAddUsersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta användare",
|
||||
"contactsSettings_autoAddRepeatersTitle": "Lägg till upprepande enheter automatiskt",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta rumsservrar.",
|
||||
"contactsSettings_autoAddSensorsTitle": "Lägg till sensorer automatiskt",
|
||||
"contactsSettings_autoAddUsersTitle": "Lägg till användare automatiskt",
|
||||
"contactsSettings_title": "Kontaktinställningar",
|
||||
"contactsSettings_autoAddSensorsSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta sensorer.",
|
||||
"contactsSettings_overwriteOldestTitle": "Skriv över äldst",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta repeater.",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Lägg automatiskt till rumsservrar",
|
||||
"discoveredContacts_noMatching": "Inga matchande kontakter",
|
||||
"discoveredContacts_searchHint": "Sök uppfunna kontakter",
|
||||
"discoveredContacts_deleteContact": "Ta bort kontakt",
|
||||
"discoveredContacts_Title": "Upptäckta kontakter",
|
||||
"discoveredContacts_contactAdded": "Kontakt tillagd",
|
||||
"discoveredContacts_addContact": "Lägg till kontakt",
|
||||
"discoveredContacts_copyContact": "Kopiera kontakt till urklipp",
|
||||
"contactsSettings_overwriteOldestSubtitle": "När kontaktlistan är full ersätts den äldsta icke-favoriterade kontakten.",
|
||||
"common_deleteAll": "Ta bort alla",
|
||||
"discoveredContacts_deleteContactAllContent": "Är du säker på att du vill ta bort alla upptäckta kontakter?",
|
||||
"discoveredContacts_deleteContactAll": "Ta bort alla upptäckta kontakter",
|
||||
"map_guessedLocation": "Gissad plats",
|
||||
"map_showGuessedLocations": "Visa upp de antagna nodernas placeringar",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"usbScreenSubtitle": "Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.",
|
||||
"usbScreenTitle": "Anslut via USB",
|
||||
"usbScreenStatus": "Välj en USB-enhet",
|
||||
"usbScreenNote": "USB-seriell kommunikation är aktiv på stödda Android-enheter och på skrivbordsplattformar.",
|
||||
"usbScreenEmptyState": "Inga USB-enheter hittades. Anslut en och uppdatera.",
|
||||
"usbErrorPermissionDenied": "Tillgången via USB nekas.",
|
||||
"usbErrorDeviceMissing": "Den valda USB-enheten är inte längre tillgänglig.",
|
||||
"usbErrorInvalidPort": "Välj en giltig USB-enhet.",
|
||||
"usbErrorBusy": "En annan förfrågan om USB-anslutning är redan pågående.",
|
||||
"usbErrorNotConnected": "Ingen USB-enhet är ansluten.",
|
||||
"usbErrorOpenFailed": "Misslyckades med att öppna det valda USB-enheten.",
|
||||
"usbErrorConnectFailed": "Kunde inte ansluta till det valda USB-enheten.",
|
||||
"usbErrorUnsupported": "USB-seriell kommunikation stöds inte på denna plattform.",
|
||||
"usbErrorAlreadyActive": "En USB-anslutning är redan aktiv.",
|
||||
"usbErrorNoDeviceSelected": "Ingen USB-enhet valdes.",
|
||||
"usbErrorPortClosed": "USB-anslutningen är inte aktiv.",
|
||||
"usbFallbackDeviceName": "Web-serieenhet",
|
||||
"@usbConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"usbStatus_connecting": "Anslutning till USB-enhet...",
|
||||
"usbStatus_notConnected": "Välj en USB-enhet",
|
||||
"usbConnectionFailed": "Fel vid USB-anslutning: {error}",
|
||||
"usbStatus_searching": "Söker efter USB-enheter...",
|
||||
"usbErrorConnectTimedOut": "Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware.",
|
||||
"@tcpStatus_connectingTo": {
|
||||
"placeholders": {
|
||||
"endpoint": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@tcpConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tcpHostHint": "192.168.40.10",
|
||||
"tcpHostLabel": "IP-adress",
|
||||
"tcpScreenTitle": "Anslut via TCP",
|
||||
"connectionChoiceTcpLabel": "TCP",
|
||||
"tcpPortLabel": "Port",
|
||||
"tcpPortHint": "5000",
|
||||
"tcpStatus_notConnected": "Ange slutpunkt och anslut",
|
||||
"tcpStatus_connectingTo": "Anslutning till {endpoint}...",
|
||||
"tcpErrorHostRequired": "IP-adress krävs.",
|
||||
"tcpErrorPortInvalid": "Porten måste vara mellan 1 och 65535.",
|
||||
"tcpErrorUnsupported": "TCP-transport fungerar inte på denna plattform.",
|
||||
"tcpErrorTimedOut": "TCP-anslutningen har tidsut gått.",
|
||||
"tcpConnectionFailed": "Fel vid TCP-anslutning: {error}",
|
||||
"map_showDiscoveryContacts": "Visa Discovery-kontakter",
|
||||
"map_setAsMyLocation": "Ange som min plats",
|
||||
"@path_routeWeight": {
|
||||
"placeholders": {
|
||||
"weight": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
"appSettings_initialRouteWeight": "Initial vikt för rutt",
|
||||
"appSettings_routeWeightSuccessIncrement": "Ökning av vikt för framgång",
|
||||
"appSettings_routeWeightSuccessIncrementSubtitle": "Vikt läggs till en väg efter en lyckad leverans.",
|
||||
"appSettings_routeWeightFailureDecrement": "Minskning av vikten för misslyckande",
|
||||
"appSettings_routeWeightFailureDecrementSubtitle": "Vikt som tagits bort från en väg efter ett misslyckat leveransförsök",
|
||||
"appSettings_maxMessageRetries": "Maximalt antal försök",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Antal försök att skicka om ett meddelande innan det markeras som misslyckat.",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@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",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_enableSubtitle": "Översätt inkommande meddelanden och möjliggör översättning före avsändning.",
|
||||
"translation_enableTitle": "Aktivera översättning",
|
||||
"translation_title": "Översättning",
|
||||
"translation_composerTitle": "Översätt innan du skickar",
|
||||
"translation_composerSubtitle": "Styr standardtillståndet för kompositorns översättningsikon.",
|
||||
"translation_targetLanguage": "Målmedvetet språk",
|
||||
"translation_useAppLanguage": "Använd appens språk",
|
||||
"translation_downloadedModelLabel": "Nedladdad modell",
|
||||
"translation_presetModelLabel": "Fördefinierat Hugging Face-modell",
|
||||
"translation_manualUrlLabel": "Manualens URL",
|
||||
"translation_downloadModel": "Ladda ner modellen",
|
||||
"translation_downloading": "Nedladdning...",
|
||||
"translation_working": "Arbeta...",
|
||||
"translation_stop": "Stopp",
|
||||
"translation_mergingChunks": "Slå samman de nedladdade delarna till en slutlig fil...",
|
||||
"translation_downloadedModels": "Nedladdade modeller",
|
||||
"translation_deleteModel": "Ta bort modell",
|
||||
"translation_modelDownloaded": "Översättningsmodellen har laddats ner.",
|
||||
"translation_downloadStopped": "Nedladdningen avbruten.",
|
||||
"translation_downloadFailed": "Nedladdning misslyckades: {error}",
|
||||
"translation_enterUrlFirst": "Ange först en URL för en specifik modell.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerDisabledHint": "Skicka meddelanden på det ursprungliga, stavade språket.",
|
||||
"translation_translateBeforeSending": "Översätt innan du skickar",
|
||||
"translation_composerEnabledHint": "Meddelandena kommer att översättas innan de skickas.",
|
||||
"translation_messageTranslation": "Meddelandets översättning",
|
||||
"translation_translateTo": "Översätt till {language}",
|
||||
"translation_translationOptions": "Översättningsalternativ",
|
||||
"translation_systemLanguage": "Språk för systemet",
|
||||
"scanner_linuxPairingShowPin": "Visa PIN",
|
||||
"scanner_linuxPairingPinTitle": "Bluetooth‑parnings‑PIN",
|
||||
"scanner_linuxPairingPinPrompt": "Ange PIN för {deviceName} (lämna tomt om ingen).",
|
||||
"scanner_linuxPairingHidePin": "Dölj PIN",
|
||||
"repeater_cliQuickDiscovery": "Upptäck grannar",
|
||||
"repeater_cliQuickClockSync": "Synkronisera klocka",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Automatiskt skicka \"klocksynkronisering\" efter en lyckad inloggning.",
|
||||
"repeater_clockSyncAfterLogin": "Synkronisera klockan efter inloggning",
|
||||
"repeater_guest": "Information om repetorer",
|
||||
"chat_sendMessage": "Skicka meddelande",
|
||||
"repeater_guestTools": "Gästverktyg",
|
||||
"room_guest": "Information om servern"
|
||||
}
|
||||
|
||||
+275
-1
@@ -286,6 +286,7 @@
|
||||
"contacts_newGroup": "Нова група",
|
||||
"contacts_groupName": "Назва групи",
|
||||
"contacts_groupNameRequired": "Назва групи обов'язкова.",
|
||||
"contacts_groupNameReserved": "Ця назва групи зарезервована",
|
||||
"contacts_groupAlreadyExists": "Група «{name}» вже існує.",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -1606,6 +1607,8 @@
|
||||
"map_pathTraceCancelled": "Відмінується трасування шляху",
|
||||
"scanner_enableBluetooth": "Увімкніть Bluetooth",
|
||||
"scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.",
|
||||
"scanner_chromeRequired": "Потрібен браузер Chrome",
|
||||
"scanner_chromeRequiredMessage": "Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.",
|
||||
"scanner_bluetoothOff": "Bluetooth вимкнено",
|
||||
"snrIndicator_lastSeen": "Останній раз бачили",
|
||||
"snrIndicator_nearByRepeaters": "Ближні ретранслятори",
|
||||
@@ -1800,5 +1803,276 @@
|
||||
"contacts_searchFavorites": "Пошук {number}{str} улюблених...",
|
||||
"contacts_searchContactsNoNumber": "Пошук контактів...",
|
||||
"contacts_searchRepeaters": "Пошук {number}{str} ретрансляторів...",
|
||||
"contacts_unread": "Непрочитане"
|
||||
"contacts_unread": "Непрочитане",
|
||||
"settings_contactSettingsSubtitle": "Налаштування для додавання контактів",
|
||||
"settings_contactSettings": "Налаштування контактів",
|
||||
"contactsSettings_autoAddUsersSubtitle": "Дозволити супутникові автоматично додавати виявлених користувачів",
|
||||
"contactsSettings_autoAddRepeatersTitle": "Автоматично додавати повторювачі",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "Дозволити супутнику автоматично додавати виявлені ретранслятори",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Автоматично додавати сервери кімнат",
|
||||
"contactsSettings_otherTitle": "Інші налаштування, пов'язані з контактами",
|
||||
"contactsSettings_autoAddTitle": "Автоматичне виявлення",
|
||||
"contactsSettings_autoAddUsersTitle": "Автоматично додавати користувачів",
|
||||
"contactsSettings_title": "Налаштування контактів",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Дозволити супровіднику автоматично додавати виявлені сервери кімнат.",
|
||||
"contactsSettings_autoAddSensorsTitle": "Автоматично додавати датчики",
|
||||
"discoveredContacts_searchHint": "Знайти виявлені контакти",
|
||||
"discoveredContacts_contactAdded": "Контакт додано",
|
||||
"contactsSettings_autoAddSensorsSubtitle": "Дозволити супровіднику автоматично додавати виявлені сенсори",
|
||||
"contactsSettings_overwriteOldestTitle": "Перезаписати найстаріше",
|
||||
"discoveredContacts_Title": "Виявлені контакти",
|
||||
"discoveredContacts_noMatching": "Відповідних контактів не знайдено",
|
||||
"discoveredContacts_deleteContact": "Видалити контакт",
|
||||
"discoveredContacts_copyContact": "Копіювати контакт у буфер обміну",
|
||||
"discoveredContacts_addContact": "Додати контакт",
|
||||
"contactsSettings_overwriteOldestSubtitle": "Коли список контактів заповнений, найстарший контакт без позначки улюбленого буде замінений.",
|
||||
"common_deleteAll": "Видалити все",
|
||||
"discoveredContacts_deleteContactAll": "Видалити всі виявлені контакти",
|
||||
"discoveredContacts_deleteContactAllContent": "Ви впевнені, що хочете видалити всі виявлені контакти?",
|
||||
"map_showGuessedLocations": "Показати місцезнаходження передбачених вузлів",
|
||||
"map_guessedLocation": "Визначено місцезнаходження",
|
||||
"usbScreenSubtitle": "Виберіть виявлене серійне пристрій і підключіть його безпосередньо до вашого вузла MeshCore.",
|
||||
"usbScreenTitle": "Підключити через USB",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"usbScreenStatus": "Виберіть пристрій USB",
|
||||
"usbScreenNote": "USB-серіальний інтерфейс активний на підтримуваних пристроях на базі Android та на десктопних платформах.",
|
||||
"usbScreenEmptyState": "Не знайдено жодних пристроїв USB. Підключіть один і перезавантажте.",
|
||||
"usbErrorPermissionDenied": "Було відмовлено у наданні дозволу на використання USB.",
|
||||
"usbErrorDeviceMissing": "Вибране USB-пристрій більше недоступне.",
|
||||
"usbErrorInvalidPort": "Виберіть дійсний USB-пристрій.",
|
||||
"usbErrorBusy": "Ще один запит на підключення через USB вже обробляється.",
|
||||
"usbErrorNotConnected": "Немає підключених пристроїв USB.",
|
||||
"usbErrorOpenFailed": "Не вдалося відкрити вибране USB-пристрій.",
|
||||
"usbErrorConnectFailed": "Не вдалося підключитися до вибраного USB-пристрою.",
|
||||
"usbErrorUnsupported": "Підтримка USB-серіального інтерфейсу не реалізована на цій платформі.",
|
||||
"usbErrorAlreadyActive": "USB-з'єднання вже встановлено.",
|
||||
"usbErrorNoDeviceSelected": "Не було вибрано жодного пристрою USB.",
|
||||
"usbErrorPortClosed": "З'єднання USB не встановлено.",
|
||||
"usbFallbackDeviceName": "Пристрій для передачі даних по веб-серіалах",
|
||||
"@usbConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"usbStatus_searching": "Пошук пристроїв USB...",
|
||||
"usbStatus_notConnected": "Виберіть пристрій USB",
|
||||
"usbConnectionFailed": "Не вдалося встановити з'єднання через USB: {error}",
|
||||
"usbStatus_connecting": "Підключення до USB-пристрою...",
|
||||
"usbErrorConnectTimedOut": "З'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion.",
|
||||
"@tcpStatus_connectingTo": {
|
||||
"placeholders": {
|
||||
"endpoint": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@tcpConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"connectionChoiceTcpLabel": "TCP",
|
||||
"tcpHostHint": "192.168.40.10",
|
||||
"tcpHostLabel": "IP-адреса",
|
||||
"tcpScreenTitle": "З'єднатися через протокол TCP",
|
||||
"tcpPortLabel": "Порт",
|
||||
"tcpPortHint": "5000",
|
||||
"tcpStatus_notConnected": "Введіть кінцеву точку та підключіться",
|
||||
"tcpStatus_connectingTo": "Підключення до {endpoint}...",
|
||||
"tcpErrorHostRequired": "Необхідно вказати IP-адресу.",
|
||||
"tcpErrorPortInvalid": "Порт повинен бути в межах від 1 до 65535.",
|
||||
"tcpErrorUnsupported": "Транспорт TCP не підтримується на цій платформі.",
|
||||
"tcpErrorTimedOut": "З'єднання TCP завершилося через закінчення часу очікування.",
|
||||
"tcpConnectionFailed": "Не вдалося встановити з'єднання TCP: {error}",
|
||||
"map_showDiscoveryContacts": "Показати контакти Відкриття",
|
||||
"map_setAsMyLocation": "Встановити моє місцезнаходження",
|
||||
"@path_routeWeight": {
|
||||
"placeholders": {
|
||||
"weight": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": "Максимальна вага маршруту",
|
||||
"appSettings_maxRouteWeightSubtitle": "Максимальна вага, яку може накопичити маршрут завдяки успішним доставкам.",
|
||||
"appSettings_routeWeightSuccessIncrement": "Збільшення ваги успіху",
|
||||
"appSettings_routeWeightSuccessIncrementSubtitle": "Вага, додана до маршруту після успішної доставки",
|
||||
"appSettings_routeWeightFailureDecrement": "Зменшення ваги помилки",
|
||||
"appSettings_routeWeightFailureDecrementSubtitle": "Вага, яка була знята з маршруту після невдалої доставки",
|
||||
"appSettings_maxMessageRetries": "Максимальна кількість повторних спроб надсилання повідомлення",
|
||||
"appSettings_maxMessageRetriesSubtitle": "Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@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 та час, протягом якого пристрій використовує радіоканал.",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerTitle": "Перекладіть перед відправкою",
|
||||
"translation_title": "Переклад",
|
||||
"translation_enableTitle": "Увімкнути переклад",
|
||||
"translation_enableSubtitle": "Перекладати отримані повідомлення та дозволяти попередній переклад перед відправкою.",
|
||||
"translation_composerSubtitle": "Контролює стан ікон перекладу, який використовується за замовчуванням.",
|
||||
"translation_targetLanguage": "Цільова мова",
|
||||
"translation_useAppLanguage": "Використовуйте мову додатку",
|
||||
"translation_downloadedModelLabel": "Завантажений шаблон",
|
||||
"translation_presetModelLabel": "Заздалегідь налаштований модель від Hugging Face",
|
||||
"translation_manualUrlLabel": "Посилання на веб-сторінку з інструкцією",
|
||||
"translation_downloadModel": "Завантажити модель",
|
||||
"translation_downloading": "Завантаження...",
|
||||
"translation_working": "Працюю...",
|
||||
"translation_stop": "Припинити",
|
||||
"translation_mergingChunks": "Об'єднання завантажених фрагментів у кінцевий файл...",
|
||||
"translation_downloadedModels": "Завантажені моделі",
|
||||
"translation_deleteModel": "Видалити модель",
|
||||
"translation_modelDownloaded": "Модель перекладу завантажена.",
|
||||
"translation_downloadStopped": "Завантаження призупинено.",
|
||||
"translation_downloadFailed": "Не вдалося завантажити: {error}",
|
||||
"translation_enterUrlFirst": "Спочатку введіть URL моделі.",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerEnabledHint": "Повідомлення будуть перекладені перед відправленням.",
|
||||
"translation_messageTranslation": "Переклад повідомлення",
|
||||
"translation_composerDisabledHint": "Надсилайте повідомлення, використовуючи оригінальний текстовий формат.",
|
||||
"translation_translateBeforeSending": "Перекладіть перед відправкою",
|
||||
"translation_translateTo": "Перекласти на {language}",
|
||||
"translation_translationOptions": "Варіанти перекладу",
|
||||
"translation_systemLanguage": "Мова системи",
|
||||
"scanner_linuxPairingPinTitle": "PIN‑код спарювання Bluetooth",
|
||||
"scanner_linuxPairingShowPin": "Показати PIN",
|
||||
"scanner_linuxPairingPinPrompt": "Введіть PIN для {deviceName} (залиште порожнім, якщо його немає).",
|
||||
"scanner_linuxPairingHidePin": "Приховати PIN",
|
||||
"repeater_cliQuickClockSync": "Синхронізація годинника",
|
||||
"repeater_cliQuickDiscovery": "Відкрити сусідів",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLoginSubtitle": "Автоматично надсилати повідомлення \"синхронізація годин\" після успішного входу.",
|
||||
"repeater_clockSyncAfterLogin": "Синхронізація годин після входу",
|
||||
"repeater_guestTools": "Інструменти для гостей",
|
||||
"repeater_guest": "Інформація про ретранслятор",
|
||||
"room_guest": "Інформація про сервер кімнати",
|
||||
"chat_sendMessage": "Надіслати повідомлення"
|
||||
}
|
||||
|
||||
+275
-1
@@ -300,6 +300,7 @@
|
||||
"contacts_newGroup": "新建群聊",
|
||||
"contacts_groupName": "群聊名称",
|
||||
"contacts_groupNameRequired": "请输入群聊名称",
|
||||
"contacts_groupNameReserved": "该群组名称已被保留",
|
||||
"contacts_groupAlreadyExists": "名为 \"{name}\" 的群聊已存在",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -1610,6 +1611,8 @@
|
||||
"map_removeLast": "移除最后一个",
|
||||
"map_runTrace": "运行路径追踪",
|
||||
"scanner_bluetoothOffMessage": "请开启蓝牙以搜索设备",
|
||||
"scanner_chromeRequired": "需要 Chrome 浏览器",
|
||||
"scanner_chromeRequiredMessage": "此 Web 应用程序需要 Google Chrome 或基于 Chromium 的浏览器以支持蓝牙。",
|
||||
"scanner_bluetoothOff": "蓝牙已关闭",
|
||||
"scanner_enableBluetooth": "启用蓝牙",
|
||||
"snrIndicator_lastSeen": "最近访问",
|
||||
@@ -1805,5 +1808,276 @@
|
||||
"contacts_searchRepeaters": "搜索 {number}{str} 重复器...",
|
||||
"contacts_searchContactsNoNumber": "搜索联系人...",
|
||||
"contacts_searchRoomServers": "搜索 {number}{str} 房间服务器...",
|
||||
"contacts_searchFavorites": "搜索 {number}{str} 收藏..."
|
||||
"contacts_searchFavorites": "搜索 {number}{str} 收藏...",
|
||||
"settings_contactSettings": "联系人设置",
|
||||
"contactsSettings_title": "联系人设置",
|
||||
"contactsSettings_autoAddUsersTitle": "自动添加用户",
|
||||
"contactsSettings_otherTitle": "其他联系人相关设置",
|
||||
"contactsSettings_autoAddUsersSubtitle": "允许伴侣自动添加发现的用户",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "允许伴侣自动添加发现的重复器",
|
||||
"contactsSettings_autoAddSensorsTitle": "自动添加传感器",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "允许伴侣自动添加发现的房间服务器",
|
||||
"contactsSettings_autoAddRepeatersTitle": "自动添加重复器",
|
||||
"contactsSettings_autoAddTitle": "自动发现",
|
||||
"settings_contactSettingsSubtitle": "添加联系人的设置",
|
||||
"contactsSettings_overwriteOldestTitle": "覆盖最旧的",
|
||||
"contactsSettings_autoAddSensorsSubtitle": "允许伴侣自动添加发现的传感器",
|
||||
"discoveredContacts_searchHint": "搜索已发现的联系人",
|
||||
"contactsSettings_autoAddRoomServersTitle": "自动添加房间服务器",
|
||||
"discoveredContacts_contactAdded": "联系人已添加",
|
||||
"discoveredContacts_deleteContact": "删除联系人",
|
||||
"discoveredContacts_addContact": "添加联系人",
|
||||
"discoveredContacts_noMatching": "没有匹配的联系人",
|
||||
"discoveredContacts_Title": "已发现的联系人",
|
||||
"discoveredContacts_copyContact": "复制联系人到剪贴板",
|
||||
"contactsSettings_overwriteOldestSubtitle": "当联系人列表已满时,将替换最老的非收藏联系人。",
|
||||
"common_deleteAll": "删除全部",
|
||||
"discoveredContacts_deleteContactAllContent": "您确定要删除所有发现的联系人吗?",
|
||||
"discoveredContacts_deleteContactAll": "删除所有发现的联系人",
|
||||
"map_showGuessedLocations": "显示猜测的节点位置",
|
||||
"map_guessedLocation": "猜测的位置",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"usbScreenTitle": "通过USB连接",
|
||||
"usbScreenSubtitle": "选择已检测到的串行设备,并直接连接到您的 MeshCore 节点。",
|
||||
"connectionChoiceBluetoothLabel": "蓝牙",
|
||||
"usbScreenStatus": "选择一个 USB 设备",
|
||||
"usbScreenNote": "USB 串行接口在支持的 Android 设备和桌面平台上处于活动状态。",
|
||||
"usbScreenEmptyState": "未找到任何 USB 设备。请插入一个,然后刷新。",
|
||||
"usbErrorPermissionDenied": "拒绝了USB权限。",
|
||||
"usbErrorDeviceMissing": "所选的USB设备已不再可用。",
|
||||
"usbErrorInvalidPort": "选择一个有效的USB设备。",
|
||||
"usbErrorBusy": "还有一个 USB 连接请求正在进行中。",
|
||||
"usbErrorNotConnected": "没有连接任何USB设备。",
|
||||
"usbErrorOpenFailed": "未能打开所选的USB设备。",
|
||||
"usbErrorConnectFailed": "未能连接到所选的USB设备。",
|
||||
"usbErrorUnsupported": "此平台不支持USB串行通信。",
|
||||
"usbErrorAlreadyActive": "USB 连接已建立。",
|
||||
"usbErrorNoDeviceSelected": "未选择任何 USB 设备。",
|
||||
"usbErrorPortClosed": "USB 连接未建立。",
|
||||
"usbFallbackDeviceName": "Web 串流设备",
|
||||
"@usbConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"usbStatus_searching": "正在搜索 USB 设备...",
|
||||
"usbStatus_connecting": "连接USB设备...",
|
||||
"usbStatus_notConnected": "选择一个 USB 设备",
|
||||
"usbConnectionFailed": "USB 连接失败:{error}",
|
||||
"usbErrorConnectTimedOut": "连接超时。请确保设备已安装 USB 伴侣固件。",
|
||||
"@tcpStatus_connectingTo": {
|
||||
"placeholders": {
|
||||
"endpoint": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@tcpConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tcpHostLabel": "IP地址",
|
||||
"tcpHostHint": "192.168.40.10",
|
||||
"tcpScreenTitle": "通过 TCP 连接",
|
||||
"connectionChoiceTcpLabel": "TCP",
|
||||
"tcpPortLabel": "端口",
|
||||
"tcpPortHint": "5000",
|
||||
"tcpStatus_notConnected": "输入目标地址,然后连接",
|
||||
"tcpStatus_connectingTo": "连接到 {endpoint}...",
|
||||
"tcpErrorHostRequired": "需要提供IP地址。",
|
||||
"tcpErrorPortInvalid": "端口号必须在 1 到 65535 之间。",
|
||||
"tcpErrorUnsupported": "此平台不支持 TCP 传输。",
|
||||
"tcpErrorTimedOut": "TCP 连接超时。",
|
||||
"tcpConnectionFailed": "TCP 连接失败:{error}",
|
||||
"map_showDiscoveryContacts": "显示发现联系人",
|
||||
"map_setAsMyLocation": "设置为我的位置",
|
||||
"@path_routeWeight": {
|
||||
"placeholders": {
|
||||
"weight": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": "初始路线权重",
|
||||
"appSettings_maxRouteWeightSubtitle": "一条路径可以累积的最大重量,取决于成功交付的数量。",
|
||||
"appSettings_routeWeightSuccessIncrement": "成功权重增加",
|
||||
"appSettings_routeWeightSuccessIncrementSubtitle": "在成功交付后,将重量添加到路径中",
|
||||
"appSettings_routeWeightFailureDecrement": "失败权重降低",
|
||||
"appSettings_routeWeightFailureDecrementSubtitle": "从一条路径上移除的货物,由于无法成功交付而移除。",
|
||||
"appSettings_maxMessageRetries": "最大消息重试次数",
|
||||
"appSettings_maxMessageRetriesSubtitle": "在将消息标记为失败之前,允许尝试的次数",
|
||||
"path_routeWeight": "{weight}/{max}",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@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、信噪比和空中时间",
|
||||
"@translation_downloadFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_title": "翻译",
|
||||
"translation_enableSubtitle": "翻译收到的消息,并允许在发送前进行翻译。",
|
||||
"translation_composerTitle": "在发送之前进行翻译",
|
||||
"translation_enableTitle": "启用翻译功能",
|
||||
"translation_composerSubtitle": "控制作曲家翻译图标的默认状态。",
|
||||
"translation_targetLanguage": "目标语言",
|
||||
"translation_useAppLanguage": "使用应用程序语言",
|
||||
"translation_downloadedModelLabel": "下载的模型",
|
||||
"translation_presetModelLabel": "预设的 Hugging Face 模型",
|
||||
"translation_downloadModel": "下载模型",
|
||||
"translation_manualUrlLabel": "手动模型网址",
|
||||
"translation_downloading": "正在下载...",
|
||||
"translation_working": "工作中...",
|
||||
"translation_stop": "停止",
|
||||
"translation_mergingChunks": "将下载的片段合并成最终文件...",
|
||||
"translation_downloadedModels": "下载的模型",
|
||||
"translation_deleteModel": "删除模型",
|
||||
"translation_modelDownloaded": "翻译模型已下载。",
|
||||
"translation_downloadStopped": "下载已停止。",
|
||||
"translation_downloadFailed": "下载失败:{error}",
|
||||
"translation_enterUrlFirst": "首先,请输入模型的 URL。",
|
||||
"@scanner_linuxPairingPinPrompt": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner_linuxPairingPinTitle": "蓝牙配对 PIN",
|
||||
"scanner_linuxPairingPinPrompt": "输入 {deviceName} 的 PIN 码(如果为空,则留空)。",
|
||||
"scanner_linuxPairingHidePin": "隐藏 PIN",
|
||||
"scanner_linuxPairingShowPin": "显示PIN码",
|
||||
"@translation_translateTo": {
|
||||
"placeholders": {
|
||||
"language": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"translation_composerDisabledHint": "使用原始的打字方式发送消息。",
|
||||
"translation_messageTranslation": "消息翻译",
|
||||
"translation_composerEnabledHint": "消息将在发送前进行翻译。",
|
||||
"translation_translateBeforeSending": "在发送前进行翻译",
|
||||
"translation_translateTo": "翻译成 {language}",
|
||||
"translation_translationOptions": "翻译选项",
|
||||
"translation_systemLanguage": "系统语言",
|
||||
"repeater_cliQuickDiscovery": "发现邻居",
|
||||
"repeater_cliQuickClockSync": "同步时钟",
|
||||
"@repeater_clockSyncAfterLogin": {
|
||||
"description": "Repeater setting: auto sync device clock after successful login"
|
||||
},
|
||||
"@repeater_clockSyncAfterLoginSubtitle": {
|
||||
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
|
||||
},
|
||||
"repeater_clockSyncAfterLogin": "登录后,自动同步时钟",
|
||||
"repeater_clockSyncAfterLoginSubtitle": "在成功登录后,自动发送“时钟同步”指令。",
|
||||
"repeater_guestTools": "访客工具",
|
||||
"repeater_guest": "重复器信息",
|
||||
"chat_sendMessage": "发送消息",
|
||||
"room_guest": "服务器信息"
|
||||
}
|
||||
|
||||
+29
-1
@@ -4,6 +4,9 @@ import 'package:flutter/foundation.dart';
|
||||
import 'l10n/app_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'screens/chrome_required_screen.dart';
|
||||
import 'utils/platform_info.dart';
|
||||
|
||||
import 'connector/meshcore_connector.dart';
|
||||
import 'screens/scanner_screen.dart';
|
||||
import 'services/storage_service.dart';
|
||||
@@ -16,6 +19,9 @@ import 'services/app_debug_log_service.dart';
|
||||
import 'services/background_service.dart';
|
||||
import 'services/map_tile_cache_service.dart';
|
||||
import 'services/chat_text_scale_service.dart';
|
||||
import 'services/translation_service.dart';
|
||||
import 'services/ui_view_state_service.dart';
|
||||
import 'services/timeout_prediction_service.dart';
|
||||
import 'storage/prefs_manager.dart';
|
||||
import 'utils/app_logger.dart';
|
||||
|
||||
@@ -36,6 +42,9 @@ void main() async {
|
||||
final backgroundService = BackgroundService();
|
||||
final mapTileCacheService = MapTileCacheService();
|
||||
final chatTextScaleService = ChatTextScaleService();
|
||||
final translationService = TranslationService(appSettingsService);
|
||||
final uiViewStateService = UiViewStateService();
|
||||
final timeoutPredictionService = TimeoutPredictionService(storage);
|
||||
|
||||
// Load settings
|
||||
await appSettingsService.loadSettings();
|
||||
@@ -53,15 +62,20 @@ void main() async {
|
||||
_registerThirdPartyLicenses();
|
||||
|
||||
await chatTextScaleService.initialize();
|
||||
await translationService.refreshDownloadedModels();
|
||||
await uiViewStateService.initialize();
|
||||
await timeoutPredictionService.initialize();
|
||||
|
||||
// Wire up connector with services
|
||||
connector.initialize(
|
||||
retryService: retryService,
|
||||
pathHistoryService: pathHistoryService,
|
||||
appSettingsService: appSettingsService,
|
||||
translationService: translationService,
|
||||
bleDebugLogService: bleDebugLogService,
|
||||
appDebugLogService: appDebugLogService,
|
||||
backgroundService: backgroundService,
|
||||
timeoutPredictionService: timeoutPredictionService,
|
||||
);
|
||||
|
||||
await connector.loadContactCache();
|
||||
@@ -83,6 +97,9 @@ void main() async {
|
||||
appDebugLogService: appDebugLogService,
|
||||
mapTileCacheService: mapTileCacheService,
|
||||
chatTextScaleService: chatTextScaleService,
|
||||
translationService: translationService,
|
||||
uiViewStateService: uiViewStateService,
|
||||
timeoutPredictionService: timeoutPredictionService,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -118,6 +135,9 @@ class MeshCoreApp extends StatelessWidget {
|
||||
final AppDebugLogService appDebugLogService;
|
||||
final MapTileCacheService mapTileCacheService;
|
||||
final ChatTextScaleService chatTextScaleService;
|
||||
final TranslationService translationService;
|
||||
final UiViewStateService uiViewStateService;
|
||||
final TimeoutPredictionService timeoutPredictionService;
|
||||
|
||||
const MeshCoreApp({
|
||||
super.key,
|
||||
@@ -130,6 +150,9 @@ class MeshCoreApp extends StatelessWidget {
|
||||
required this.appDebugLogService,
|
||||
required this.mapTileCacheService,
|
||||
required this.chatTextScaleService,
|
||||
required this.translationService,
|
||||
required this.uiViewStateService,
|
||||
required this.timeoutPredictionService,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -143,8 +166,11 @@ class MeshCoreApp extends StatelessWidget {
|
||||
ChangeNotifierProvider.value(value: bleDebugLogService),
|
||||
ChangeNotifierProvider.value(value: appDebugLogService),
|
||||
ChangeNotifierProvider.value(value: chatTextScaleService),
|
||||
ChangeNotifierProvider.value(value: translationService),
|
||||
ChangeNotifierProvider.value(value: uiViewStateService),
|
||||
Provider.value(value: storage),
|
||||
Provider.value(value: mapTileCacheService),
|
||||
ChangeNotifierProvider.value(value: timeoutPredictionService),
|
||||
],
|
||||
child: Consumer<AppSettingsService>(
|
||||
builder: (context, settingsService, child) {
|
||||
@@ -187,7 +213,9 @@ class MeshCoreApp extends StatelessWidget {
|
||||
NotificationService().setLocale(locale);
|
||||
return child ?? const SizedBox.shrink();
|
||||
},
|
||||
home: const ScannerScreen(),
|
||||
home: (PlatformInfo.isWeb && !PlatformInfo.isChrome)
|
||||
? const ChromeRequiredScreen()
|
||||
: const ScannerScreen(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'translation_support.dart';
|
||||
|
||||
enum UnitSystem { metric, imperial }
|
||||
|
||||
extension UnitSystemValue on UnitSystem {
|
||||
@@ -18,10 +20,12 @@ 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;
|
||||
final bool mapShowMarkers;
|
||||
final bool mapShowGuessedLocations;
|
||||
final bool enableMessageTracing;
|
||||
final Map<String, double>? mapCacheBounds;
|
||||
final int mapCacheMinZoom;
|
||||
@@ -31,6 +35,11 @@ class AppSettings {
|
||||
final bool notifyOnNewChannelMessage;
|
||||
final bool notifyOnNewAdvert;
|
||||
final bool autoRouteRotationEnabled;
|
||||
final double maxRouteWeight;
|
||||
final double initialRouteWeight;
|
||||
final double routeWeightSuccessIncrement;
|
||||
final double routeWeightFailureDecrement;
|
||||
final int maxMessageRetries;
|
||||
final String themeMode;
|
||||
final String? languageOverride; // null = system default
|
||||
final bool appDebugLogEnabled;
|
||||
@@ -38,16 +47,28 @@ class AppSettings {
|
||||
final Map<String, String> batteryChemistryByRepeaterId;
|
||||
final UnitSystem unitSystem;
|
||||
final Set<String> mutedChannels;
|
||||
final bool mapShowDiscoveryContacts;
|
||||
final String tcpServerAddress;
|
||||
final int tcpServerPort;
|
||||
final bool jumpToOldestUnread;
|
||||
final bool translationEnabled;
|
||||
final String? translationTargetLanguageCode;
|
||||
final bool composerTranslationEnabled;
|
||||
final String? translationModelSourceUrl;
|
||||
final String? translationSelectedModelId;
|
||||
final List<TranslationModelRecord> translationDownloadedModels;
|
||||
|
||||
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 = '',
|
||||
this.mapShowMarkers = true,
|
||||
this.mapShowGuessedLocations = true,
|
||||
this.enableMessageTracing = false,
|
||||
this.mapCacheBounds,
|
||||
this.mapCacheMinZoom = 10,
|
||||
@@ -57,6 +78,11 @@ class AppSettings {
|
||||
this.notifyOnNewChannelMessage = true,
|
||||
this.notifyOnNewAdvert = true,
|
||||
this.autoRouteRotationEnabled = false,
|
||||
this.maxRouteWeight = 5.0,
|
||||
this.initialRouteWeight = 3.0,
|
||||
this.routeWeightSuccessIncrement = 0.5,
|
||||
this.routeWeightFailureDecrement = 0.2,
|
||||
this.maxMessageRetries = 5,
|
||||
this.themeMode = 'system',
|
||||
this.languageOverride,
|
||||
this.appDebugLogEnabled = false,
|
||||
@@ -64,9 +90,20 @@ class AppSettings {
|
||||
Map<String, String>? batteryChemistryByRepeaterId,
|
||||
this.unitSystem = UnitSystem.metric,
|
||||
Set<String>? mutedChannels,
|
||||
this.mapShowDiscoveryContacts = true,
|
||||
this.tcpServerAddress = '',
|
||||
this.tcpServerPort = 0,
|
||||
this.jumpToOldestUnread = false,
|
||||
this.translationEnabled = false,
|
||||
this.translationTargetLanguageCode,
|
||||
this.composerTranslationEnabled = false,
|
||||
this.translationModelSourceUrl,
|
||||
this.translationSelectedModelId,
|
||||
List<TranslationModelRecord>? translationDownloadedModels,
|
||||
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
|
||||
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
|
||||
mutedChannels = mutedChannels ?? {};
|
||||
mutedChannels = mutedChannels ?? {},
|
||||
translationDownloadedModels = translationDownloadedModels ?? const [];
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
@@ -74,10 +111,12 @@ 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,
|
||||
'map_show_markers': mapShowMarkers,
|
||||
'map_show_guessed_locations': mapShowGuessedLocations,
|
||||
'enable_message_tracing': enableMessageTracing,
|
||||
'map_cache_bounds': mapCacheBounds,
|
||||
'map_cache_min_zoom': mapCacheMinZoom,
|
||||
@@ -87,6 +126,11 @@ class AppSettings {
|
||||
'notify_on_new_channel_message': notifyOnNewChannelMessage,
|
||||
'notify_on_new_advert': notifyOnNewAdvert,
|
||||
'auto_route_rotation_enabled': autoRouteRotationEnabled,
|
||||
'max_route_weight': maxRouteWeight,
|
||||
'initial_route_weight': initialRouteWeight,
|
||||
'route_weight_success_increment': routeWeightSuccessIncrement,
|
||||
'route_weight_failure_decrement': routeWeightFailureDecrement,
|
||||
'max_message_retries': maxMessageRetries,
|
||||
'theme_mode': themeMode,
|
||||
'language_override': languageOverride,
|
||||
'app_debug_log_enabled': appDebugLogEnabled,
|
||||
@@ -94,6 +138,18 @@ class AppSettings {
|
||||
'battery_chemistry_by_repeater_id': batteryChemistryByRepeaterId,
|
||||
'unit_system': unitSystem.value,
|
||||
'muted_channels': mutedChannels.toList(),
|
||||
'map_show_discovery_contacts': mapShowDiscoveryContacts,
|
||||
'tcp_server_address': tcpServerAddress,
|
||||
'tcp_server_port': tcpServerPort,
|
||||
'jump_to_oldest_unread': jumpToOldestUnread,
|
||||
'translation_enabled': translationEnabled,
|
||||
'translation_target_language_code': translationTargetLanguageCode,
|
||||
'composer_translation_enabled': composerTranslationEnabled,
|
||||
'translation_model_source_url': translationModelSourceUrl,
|
||||
'translation_selected_model_id': translationSelectedModelId,
|
||||
'translation_downloaded_models': translationDownloadedModels
|
||||
.map((model) => model.toJson())
|
||||
.toList(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -110,11 +166,14 @@ 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,
|
||||
mapKeyPrefix: json['map_key_prefix'] as String? ?? '',
|
||||
mapShowMarkers: json['map_show_markers'] as bool? ?? true,
|
||||
mapShowGuessedLocations:
|
||||
json['map_show_guessed_locations'] as bool? ?? true,
|
||||
enableMessageTracing: json['enable_message_tracing'] as bool? ?? false,
|
||||
mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map(
|
||||
(key, value) => MapEntry(key.toString(), (value as num).toDouble()),
|
||||
@@ -128,6 +187,14 @@ class AppSettings {
|
||||
notifyOnNewAdvert: json['notify_on_new_advert'] as bool? ?? true,
|
||||
autoRouteRotationEnabled:
|
||||
json['auto_route_rotation_enabled'] as bool? ?? false,
|
||||
maxRouteWeight: (json['max_route_weight'] as num?)?.toDouble() ?? 5.0,
|
||||
initialRouteWeight:
|
||||
(json['initial_route_weight'] as num?)?.toDouble() ?? 3.0,
|
||||
routeWeightSuccessIncrement:
|
||||
(json['route_weight_success_increment'] as num?)?.toDouble() ?? 0.5,
|
||||
routeWeightFailureDecrement:
|
||||
(json['route_weight_failure_decrement'] as num?)?.toDouble() ?? 0.2,
|
||||
maxMessageRetries: json['max_message_retries'] as int? ?? 5,
|
||||
themeMode: json['theme_mode'] as String? ?? 'system',
|
||||
languageOverride: json['language_override'] as String?,
|
||||
appDebugLogEnabled: json['app_debug_log_enabled'] as bool? ?? false,
|
||||
@@ -147,6 +214,29 @@ class AppSettings {
|
||||
?.map((e) => e.toString())
|
||||
.toSet()) ??
|
||||
{},
|
||||
mapShowDiscoveryContacts:
|
||||
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,
|
||||
translationEnabled: json['translation_enabled'] as bool? ?? false,
|
||||
translationTargetLanguageCode:
|
||||
json['translation_target_language_code'] as String?,
|
||||
composerTranslationEnabled:
|
||||
json['composer_translation_enabled'] as bool? ?? false,
|
||||
translationModelSourceUrl:
|
||||
json['translation_model_source_url'] as String?,
|
||||
translationSelectedModelId:
|
||||
json['translation_selected_model_id'] as String?,
|
||||
translationDownloadedModels:
|
||||
(json['translation_downloaded_models'] as List<dynamic>?)
|
||||
?.map(
|
||||
(entry) => TranslationModelRecord.fromJson(
|
||||
Map<String, dynamic>.from(entry as Map),
|
||||
),
|
||||
)
|
||||
.toList() ??
|
||||
const [],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -155,10 +245,12 @@ class AppSettings {
|
||||
bool? mapShowRepeaters,
|
||||
bool? mapShowChatNodes,
|
||||
bool? mapShowOtherNodes,
|
||||
bool? mapShowOverlaps,
|
||||
double? mapTimeFilterHours,
|
||||
bool? mapKeyPrefixEnabled,
|
||||
String? mapKeyPrefix,
|
||||
bool? mapShowMarkers,
|
||||
bool? mapShowGuessedLocations,
|
||||
bool? enableMessageTracing,
|
||||
Object? mapCacheBounds = _unset,
|
||||
int? mapCacheMinZoom,
|
||||
@@ -168,6 +260,11 @@ class AppSettings {
|
||||
bool? notifyOnNewChannelMessage,
|
||||
bool? notifyOnNewAdvert,
|
||||
bool? autoRouteRotationEnabled,
|
||||
double? maxRouteWeight,
|
||||
double? initialRouteWeight,
|
||||
double? routeWeightSuccessIncrement,
|
||||
double? routeWeightFailureDecrement,
|
||||
int? maxMessageRetries,
|
||||
String? themeMode,
|
||||
Object? languageOverride = _unset,
|
||||
bool? appDebugLogEnabled,
|
||||
@@ -175,16 +272,29 @@ class AppSettings {
|
||||
Map<String, String>? batteryChemistryByRepeaterId,
|
||||
UnitSystem? unitSystem,
|
||||
Set<String>? mutedChannels,
|
||||
bool? mapShowDiscoveryContacts,
|
||||
String? tcpServerAddress,
|
||||
int? tcpServerPort,
|
||||
bool? jumpToOldestUnread,
|
||||
bool? translationEnabled,
|
||||
Object? translationTargetLanguageCode = _unset,
|
||||
bool? composerTranslationEnabled,
|
||||
Object? translationModelSourceUrl = _unset,
|
||||
Object? translationSelectedModelId = _unset,
|
||||
List<TranslationModelRecord>? translationDownloadedModels,
|
||||
}) {
|
||||
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,
|
||||
mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers,
|
||||
mapShowGuessedLocations:
|
||||
mapShowGuessedLocations ?? this.mapShowGuessedLocations,
|
||||
enableMessageTracing: enableMessageTracing ?? this.enableMessageTracing,
|
||||
mapCacheBounds: mapCacheBounds == _unset
|
||||
? this.mapCacheBounds
|
||||
@@ -198,6 +308,13 @@ class AppSettings {
|
||||
notifyOnNewAdvert: notifyOnNewAdvert ?? this.notifyOnNewAdvert,
|
||||
autoRouteRotationEnabled:
|
||||
autoRouteRotationEnabled ?? this.autoRouteRotationEnabled,
|
||||
maxRouteWeight: maxRouteWeight ?? this.maxRouteWeight,
|
||||
initialRouteWeight: initialRouteWeight ?? this.initialRouteWeight,
|
||||
routeWeightSuccessIncrement:
|
||||
routeWeightSuccessIncrement ?? this.routeWeightSuccessIncrement,
|
||||
routeWeightFailureDecrement:
|
||||
routeWeightFailureDecrement ?? this.routeWeightFailureDecrement,
|
||||
maxMessageRetries: maxMessageRetries ?? this.maxMessageRetries,
|
||||
themeMode: themeMode ?? this.themeMode,
|
||||
languageOverride: languageOverride == _unset
|
||||
? this.languageOverride
|
||||
@@ -209,6 +326,25 @@ class AppSettings {
|
||||
batteryChemistryByRepeaterId ?? this.batteryChemistryByRepeaterId,
|
||||
unitSystem: unitSystem ?? this.unitSystem,
|
||||
mutedChannels: mutedChannels ?? this.mutedChannels,
|
||||
mapShowDiscoveryContacts:
|
||||
mapShowDiscoveryContacts ?? this.mapShowDiscoveryContacts,
|
||||
tcpServerAddress: tcpServerAddress ?? this.tcpServerAddress,
|
||||
tcpServerPort: tcpServerPort ?? this.tcpServerPort,
|
||||
jumpToOldestUnread: jumpToOldestUnread ?? this.jumpToOldestUnread,
|
||||
translationEnabled: translationEnabled ?? this.translationEnabled,
|
||||
translationTargetLanguageCode: translationTargetLanguageCode == _unset
|
||||
? this.translationTargetLanguageCode
|
||||
: translationTargetLanguageCode as String?,
|
||||
composerTranslationEnabled:
|
||||
composerTranslationEnabled ?? this.composerTranslationEnabled,
|
||||
translationModelSourceUrl: translationModelSourceUrl == _unset
|
||||
? this.translationModelSourceUrl
|
||||
: translationModelSourceUrl as String?,
|
||||
translationSelectedModelId: translationSelectedModelId == _unset
|
||||
? this.translationSelectedModelId
|
||||
: translationSelectedModelId as String?,
|
||||
translationDownloadedModels:
|
||||
translationDownloadedModels ?? this.translationDownloadedModels,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+12
-9
@@ -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) {
|
||||
|
||||
+114
-79
@@ -2,6 +2,8 @@ import 'dart:typed_data';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../helpers/reaction_helper.dart';
|
||||
import '../helpers/smaz.dart';
|
||||
import 'translation_support.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
|
||||
enum ChannelMessageStatus { pending, sent, failed }
|
||||
|
||||
@@ -23,9 +25,16 @@ class Repeat {
|
||||
}
|
||||
|
||||
class ChannelMessage {
|
||||
static const Object _unset = Object();
|
||||
|
||||
final Uint8List? senderKey;
|
||||
final String senderName;
|
||||
final String text;
|
||||
final String? originalText;
|
||||
final String? translatedText;
|
||||
final String? translatedLanguageCode;
|
||||
final MessageTranslationStatus translationStatus;
|
||||
final String? translationModelId;
|
||||
final DateTime timestamp;
|
||||
final bool isOutgoing;
|
||||
final ChannelMessageStatus status;
|
||||
@@ -36,6 +45,7 @@ class ChannelMessage {
|
||||
final List<Uint8List> pathVariants;
|
||||
final int? channelIndex;
|
||||
final String messageId;
|
||||
final String? packetHash;
|
||||
final String? replyToMessageId;
|
||||
final String? replyToSenderName;
|
||||
final String? replyToText;
|
||||
@@ -45,6 +55,11 @@ class ChannelMessage {
|
||||
this.senderKey,
|
||||
required this.senderName,
|
||||
required this.text,
|
||||
this.originalText,
|
||||
this.translatedText,
|
||||
this.translatedLanguageCode,
|
||||
this.translationStatus = MessageTranslationStatus.none,
|
||||
this.translationModelId,
|
||||
required this.timestamp,
|
||||
required this.isOutgoing,
|
||||
this.status = ChannelMessageStatus.pending,
|
||||
@@ -55,6 +70,7 @@ class ChannelMessage {
|
||||
List<Uint8List>? pathVariants,
|
||||
this.channelIndex,
|
||||
String? messageId,
|
||||
this.packetHash,
|
||||
this.replyToMessageId,
|
||||
this.replyToSenderName,
|
||||
this.replyToText,
|
||||
@@ -79,15 +95,34 @@ class ChannelMessage {
|
||||
int? pathLength,
|
||||
Uint8List? pathBytes,
|
||||
List<Uint8List>? pathVariants,
|
||||
String? packetHash,
|
||||
String? replyToMessageId,
|
||||
String? replyToSenderName,
|
||||
String? replyToText,
|
||||
Object? originalText = _unset,
|
||||
Object? translatedText = _unset,
|
||||
Object? translatedLanguageCode = _unset,
|
||||
MessageTranslationStatus? translationStatus,
|
||||
Object? translationModelId = _unset,
|
||||
Map<String, int>? reactions,
|
||||
}) {
|
||||
return ChannelMessage(
|
||||
senderKey: senderKey,
|
||||
senderName: senderName,
|
||||
text: text,
|
||||
originalText: originalText == _unset
|
||||
? this.originalText
|
||||
: originalText as String?,
|
||||
translatedText: translatedText == _unset
|
||||
? this.translatedText
|
||||
: translatedText as String?,
|
||||
translatedLanguageCode: translatedLanguageCode == _unset
|
||||
? this.translatedLanguageCode
|
||||
: translatedLanguageCode as String?,
|
||||
translationStatus: translationStatus ?? this.translationStatus,
|
||||
translationModelId: translationModelId == _unset
|
||||
? this.translationModelId
|
||||
: translationModelId as String?,
|
||||
timestamp: timestamp,
|
||||
isOutgoing: isOutgoing,
|
||||
status: status ?? this.status,
|
||||
@@ -98,6 +133,7 @@ class ChannelMessage {
|
||||
pathVariants: pathVariants ?? this.pathVariants,
|
||||
channelIndex: channelIndex,
|
||||
messageId: messageId,
|
||||
packetHash: packetHash ?? this.packetHash,
|
||||
replyToMessageId: replyToMessageId ?? this.replyToMessageId,
|
||||
replyToSenderName: replyToSenderName ?? this.replyToSenderName,
|
||||
replyToText: replyToText ?? this.replyToText,
|
||||
@@ -105,100 +141,99 @@ 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(
|
||||
String text,
|
||||
String senderName,
|
||||
int channelIndex,
|
||||
) {
|
||||
int channelIndex, {
|
||||
String? originalText,
|
||||
String? translatedLanguageCode,
|
||||
String? translationModelId,
|
||||
}) {
|
||||
return ChannelMessage(
|
||||
senderKey: null,
|
||||
senderName: senderName,
|
||||
text: text,
|
||||
originalText: originalText,
|
||||
translatedLanguageCode: translatedLanguageCode,
|
||||
translationModelId: translationModelId,
|
||||
timestamp: DateTime.now(),
|
||||
isOutgoing: true,
|
||||
status: ChannelMessageStatus.pending,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+73
-67
@@ -1,4 +1,6 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:meshcore_open/utils/app_logger.dart';
|
||||
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
|
||||
class Contact {
|
||||
@@ -15,6 +17,9 @@ class Contact {
|
||||
final double? longitude;
|
||||
final DateTime lastSeen;
|
||||
final DateTime lastMessageAt;
|
||||
final bool isActive;
|
||||
final bool wasPulled;
|
||||
final Uint8List? rawPacket;
|
||||
|
||||
Contact({
|
||||
required this.publicKey,
|
||||
@@ -29,6 +34,9 @@ class Contact {
|
||||
this.longitude,
|
||||
required this.lastSeen,
|
||||
DateTime? lastMessageAt,
|
||||
this.isActive = true,
|
||||
this.wasPulled = false,
|
||||
this.rawPacket,
|
||||
}) : lastMessageAt = lastMessageAt ?? lastSeen;
|
||||
|
||||
String get publicKeyHex => pubKeyToHex(publicKey);
|
||||
@@ -59,7 +67,17 @@ class Contact {
|
||||
return '$pathLength hops';
|
||||
}
|
||||
|
||||
bool get hasLocation => latitude != null && longitude != null;
|
||||
bool get hasLocation {
|
||||
const double epsilon = 1e-6;
|
||||
final lat = latitude ?? 0.0;
|
||||
final lon = longitude ?? 0.0;
|
||||
return (lat.abs() > epsilon || lon.abs() > epsilon) &&
|
||||
lat >= -90.0 &&
|
||||
lat <= 90.0 &&
|
||||
lon >= -180.0 &&
|
||||
lon <= 180.0;
|
||||
}
|
||||
|
||||
bool get isFavorite => (flags & contactFlagFavorite) != 0;
|
||||
|
||||
Contact copyWith({
|
||||
@@ -76,6 +94,8 @@ class Contact {
|
||||
double? longitude,
|
||||
DateTime? lastSeen,
|
||||
DateTime? lastMessageAt,
|
||||
bool? isActive,
|
||||
Uint8List? rawPacket,
|
||||
}) {
|
||||
return Contact(
|
||||
publicKey: publicKey ?? this.publicKey,
|
||||
@@ -94,18 +114,19 @@ class Contact {
|
||||
longitude: longitude ?? this.longitude,
|
||||
lastSeen: lastSeen ?? this.lastSeen,
|
||||
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
|
||||
isActive: isActive ?? this.isActive,
|
||||
rawPacket: rawPacket ?? this.rawPacket,
|
||||
);
|
||||
}
|
||||
|
||||
String get pathIdList {
|
||||
final pathBytes = _pathBytesForDisplay;
|
||||
/// 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
|
||||
@@ -116,47 +137,14 @@ 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)}>";
|
||||
}
|
||||
|
||||
Uint8List? get traceRouteBytes {
|
||||
final pathBytes = _pathBytesForDisplay;
|
||||
Uint8List? traceBytes;
|
||||
|
||||
if (pathBytes.isEmpty) {
|
||||
traceBytes = Uint8List(1);
|
||||
traceBytes[0] = publicKey[0];
|
||||
return traceBytes;
|
||||
}
|
||||
|
||||
if (type == advTypeRepeater || type == advTypeRoom) {
|
||||
final len = (pathBytes.length + pathBytes.length + 1);
|
||||
traceBytes = Uint8List(len);
|
||||
traceBytes[pathBytes.length] = publicKey[0];
|
||||
for (int i = 0; i < pathBytes.length; i++) {
|
||||
traceBytes[i] = pathBytes[i];
|
||||
if (i < pathBytes.length) {
|
||||
traceBytes[len - 1 - i] = pathBytes[i];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (pathBytes.length < 2) {
|
||||
return pathBytes[0] == 0 ? null : pathBytes;
|
||||
}
|
||||
final len = (pathBytes.length + pathBytes.length - 1);
|
||||
traceBytes = Uint8List(len);
|
||||
for (int i = 0; i < pathBytes.length; i++) {
|
||||
traceBytes[i] = pathBytes[i];
|
||||
if (i < pathBytes.length - 1) {
|
||||
traceBytes[len - 1 - i] = pathBytes[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
return traceBytes;
|
||||
}
|
||||
|
||||
Uint8List get _pathBytesForDisplay {
|
||||
Uint8List get pathBytesForDisplay {
|
||||
if (pathOverride != null) {
|
||||
if (pathOverride! < 0) return Uint8List(0);
|
||||
return pathOverrideBytes ?? Uint8List(0);
|
||||
@@ -166,31 +154,44 @@ class Contact {
|
||||
|
||||
static Contact? fromFrame(Uint8List data) {
|
||||
if (data.isEmpty) return null;
|
||||
if (data[0] != respCodeContact) return null;
|
||||
final reader = BufferReader(data);
|
||||
try {
|
||||
final pubKey = Uint8List.fromList(
|
||||
data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize),
|
||||
);
|
||||
final type = data[contactTypeOffset];
|
||||
final flags = data[contactFlagsOffset];
|
||||
final pathLen = data[contactPathLenOffset].toSigned(8);
|
||||
final respCode = reader.readByte();
|
||||
if (respCode != respCodeContact && respCode != pushCodeNewAdvert) {
|
||||
return null;
|
||||
}
|
||||
final pubKey = reader.readBytes(pubKeySize);
|
||||
|
||||
// Guard: reject contacts with zeroed or mostly-zeroed public keys
|
||||
// (indicates corrupt flash storage on the firmware side)
|
||||
final zeroCount = pubKey.where((b) => b == 0).length;
|
||||
if (zeroCount > pubKeySize ~/ 2) return null;
|
||||
|
||||
final type = reader.readByte();
|
||||
final flags = reader.readByte();
|
||||
final pathLen = reader.readByte();
|
||||
final safePathLen = pathLen > 0
|
||||
? (pathLen > maxPathSize ? maxPathSize : pathLen)
|
||||
: 0;
|
||||
final pathBytes = safePathLen > 0
|
||||
? Uint8List.fromList(
|
||||
data.sublist(contactPathOffset, contactPathOffset + safePathLen),
|
||||
)
|
||||
: Uint8List(0);
|
||||
final name = readCString(data, contactNameOffset, maxNameSize);
|
||||
final lastmod = readUint32LE(data, contactLastmodOffset);
|
||||
final pathBytes = reader.readBytes(maxPathSize).sublist(0, safePathLen);
|
||||
final name = reader.readCStringGreedy(maxNameSize);
|
||||
|
||||
// Guard: reject contacts with non-printable names (corrupt flash data)
|
||||
if (name.isNotEmpty &&
|
||||
name.codeUnits.every((c) => c < 0x20 || c == 0xFFFD)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final lastMod = reader.readUInt32LE();
|
||||
|
||||
double? lat, lon;
|
||||
final latRaw = readInt32LE(data, contactLatOffset);
|
||||
final lonRaw = readInt32LE(data, contactLonOffset);
|
||||
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(
|
||||
@@ -198,14 +199,16 @@ class Contact {
|
||||
name: name.isEmpty ? 'Unknown' : name,
|
||||
type: type,
|
||||
flags: flags,
|
||||
pathLength: pathLen,
|
||||
pathLength: (pathLen == 0xFF || pathLen > maxPathSize) ? -1 : pathLen,
|
||||
path: pathBytes,
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000),
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastMod * 1000),
|
||||
isActive: true,
|
||||
rawPacket: null,
|
||||
);
|
||||
} catch (e) {
|
||||
// If parsing fails, return null
|
||||
appLogger.error('Failed to parse contact frame: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -216,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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
class DeliveryObservation {
|
||||
final String contactKey;
|
||||
final int pathLength;
|
||||
final int messageBytes;
|
||||
final int secondsSinceLastRx;
|
||||
final bool isFlood;
|
||||
final int deliveryMs;
|
||||
final DateTime timestamp;
|
||||
|
||||
DeliveryObservation({
|
||||
required this.contactKey,
|
||||
required this.pathLength,
|
||||
required this.messageBytes,
|
||||
required this.secondsSinceLastRx,
|
||||
required this.isFlood,
|
||||
required this.deliveryMs,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'contact_key': contactKey,
|
||||
'path_length': pathLength,
|
||||
'message_bytes': messageBytes,
|
||||
'seconds_since_last_rx': secondsSinceLastRx,
|
||||
'is_flood': isFlood,
|
||||
'delivery_ms': deliveryMs,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
factory DeliveryObservation.fromJson(Map<String, dynamic> json) {
|
||||
return DeliveryObservation(
|
||||
contactKey: json['contact_key'] as String,
|
||||
pathLength: json['path_length'] as int,
|
||||
messageBytes: json['message_bytes'] as int,
|
||||
secondsSinceLastRx: json['seconds_since_last_rx'] as int? ?? 0,
|
||||
isFlood: json['is_flood'] as bool,
|
||||
deliveryMs: json['delivery_ms'] as int,
|
||||
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
+77
-30
@@ -1,28 +1,37 @@
|
||||
import 'dart:typed_data';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../helpers/reaction_helper.dart';
|
||||
import 'translation_support.dart';
|
||||
|
||||
enum MessageStatus { pending, sent, delivered, failed }
|
||||
|
||||
class Message {
|
||||
static const Object _unset = Object();
|
||||
|
||||
final Uint8List senderKey;
|
||||
final String text;
|
||||
final DateTime timestamp;
|
||||
final bool isOutgoing;
|
||||
final bool isCli;
|
||||
final MessageStatus status;
|
||||
final String? originalText;
|
||||
final String? translatedText;
|
||||
final String? translatedLanguageCode;
|
||||
final MessageTranslationStatus translationStatus;
|
||||
final String? translationModelId;
|
||||
|
||||
// NEW: Retry logic fields
|
||||
final String? messageId;
|
||||
final String messageId;
|
||||
final int retryCount;
|
||||
final int? estimatedTimeoutMs;
|
||||
final Uint8List? expectedAckHash;
|
||||
final int? expectedAckHash;
|
||||
final DateTime? sentAt;
|
||||
final DateTime? deliveredAt;
|
||||
final int? tripTimeMs;
|
||||
final int? pathLength;
|
||||
final Uint8List pathBytes;
|
||||
final Map<String, int> reactions;
|
||||
final Map<String, MessageStatus> reactionStatuses;
|
||||
final Uint8List fourByteRoomContactKey;
|
||||
|
||||
Message({
|
||||
@@ -32,7 +41,12 @@ class Message {
|
||||
required this.isOutgoing,
|
||||
this.isCli = false,
|
||||
this.status = MessageStatus.pending,
|
||||
this.messageId,
|
||||
String? messageId,
|
||||
this.originalText,
|
||||
this.translatedText,
|
||||
this.translatedLanguageCode,
|
||||
this.translationStatus = MessageTranslationStatus.none,
|
||||
this.translationModelId,
|
||||
this.retryCount = 0,
|
||||
this.estimatedTimeoutMs,
|
||||
this.expectedAckHash,
|
||||
@@ -43,9 +57,14 @@ class Message {
|
||||
Uint8List? pathBytes,
|
||||
Uint8List? fourByteRoomContactKey,
|
||||
Map<String, int>? reactions,
|
||||
}) : pathBytes = pathBytes ?? Uint8List(0),
|
||||
Map<String, MessageStatus>? reactionStatuses,
|
||||
}) : messageId =
|
||||
messageId ??
|
||||
'${timestamp.millisecondsSinceEpoch}_${pubKeyToHex(senderKey)}_${text.hashCode}',
|
||||
pathBytes = pathBytes ?? Uint8List(0),
|
||||
fourByteRoomContactKey = fourByteRoomContactKey ?? Uint8List(0),
|
||||
reactions = reactions ?? {};
|
||||
reactions = reactions ?? {},
|
||||
reactionStatuses = reactionStatuses ?? {};
|
||||
|
||||
String get senderKeyHex => pubKeyToHex(senderKey);
|
||||
|
||||
@@ -53,14 +72,20 @@ class Message {
|
||||
MessageStatus? status,
|
||||
int? retryCount,
|
||||
int? estimatedTimeoutMs,
|
||||
Uint8List? expectedAckHash,
|
||||
int? expectedAckHash,
|
||||
DateTime? sentAt,
|
||||
DateTime? deliveredAt,
|
||||
int? tripTimeMs,
|
||||
int? pathLength,
|
||||
Uint8List? pathBytes,
|
||||
bool? isCli,
|
||||
Object? originalText = _unset,
|
||||
Object? translatedText = _unset,
|
||||
Object? translatedLanguageCode = _unset,
|
||||
MessageTranslationStatus? translationStatus,
|
||||
Object? translationModelId = _unset,
|
||||
Map<String, int>? reactions,
|
||||
Map<String, MessageStatus>? reactionStatuses,
|
||||
Uint8List? fourByteRoomContactKey,
|
||||
}) {
|
||||
return Message(
|
||||
@@ -71,6 +96,19 @@ class Message {
|
||||
isCli: isCli ?? this.isCli,
|
||||
status: status ?? this.status,
|
||||
messageId: messageId,
|
||||
originalText: originalText == _unset
|
||||
? this.originalText
|
||||
: originalText as String?,
|
||||
translatedText: translatedText == _unset
|
||||
? this.translatedText
|
||||
: translatedText as String?,
|
||||
translatedLanguageCode: translatedLanguageCode == _unset
|
||||
? this.translatedLanguageCode
|
||||
: translatedLanguageCode as String?,
|
||||
translationStatus: translationStatus ?? this.translationStatus,
|
||||
translationModelId: translationModelId == _unset
|
||||
? this.translationModelId
|
||||
: translationModelId as String?,
|
||||
retryCount: retryCount ?? this.retryCount,
|
||||
estimatedTimeoutMs: estimatedTimeoutMs ?? this.estimatedTimeoutMs,
|
||||
expectedAckHash: expectedAckHash ?? this.expectedAckHash,
|
||||
@@ -80,49 +118,58 @@ class Message {
|
||||
pathLength: pathLength ?? this.pathLength,
|
||||
pathBytes: pathBytes ?? this.pathBytes,
|
||||
reactions: reactions ?? this.reactions,
|
||||
reactionStatuses: reactionStatuses ?? this.reactionStatuses,
|
||||
fourByteRoomContactKey:
|
||||
fourByteRoomContactKey ?? this.fourByteRoomContactKey,
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
Uint8List recipientKey,
|
||||
String text, {
|
||||
String? originalText,
|
||||
String? translatedLanguageCode,
|
||||
String? translationModelId,
|
||||
int? pathLength,
|
||||
Uint8List? pathBytes,
|
||||
}) {
|
||||
return Message(
|
||||
senderKey: recipientKey,
|
||||
text: text,
|
||||
originalText: originalText,
|
||||
translatedLanguageCode: translatedLanguageCode,
|
||||
translationModelId: translationModelId,
|
||||
timestamp: DateTime.now(),
|
||||
isOutgoing: true,
|
||||
isCli: false,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
class PathRecord {
|
||||
final int hopCount;
|
||||
final int tripTimeMs;
|
||||
final DateTime timestamp;
|
||||
final DateTime? timestamp;
|
||||
final bool wasFloodDiscovery;
|
||||
final List<int> pathBytes;
|
||||
final int successCount;
|
||||
final int failureCount;
|
||||
final double routeWeight;
|
||||
|
||||
PathRecord({
|
||||
required this.hopCount,
|
||||
@@ -15,6 +16,7 @@ class PathRecord {
|
||||
required this.pathBytes,
|
||||
required this.successCount,
|
||||
required this.failureCount,
|
||||
this.routeWeight = 1.0,
|
||||
});
|
||||
|
||||
String get displayText =>
|
||||
@@ -24,11 +26,12 @@ class PathRecord {
|
||||
return {
|
||||
'hop_count': hopCount,
|
||||
'trip_time_ms': tripTimeMs,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
'timestamp': timestamp?.toIso8601String(),
|
||||
'was_flood': wasFloodDiscovery,
|
||||
'path_bytes': pathBytes,
|
||||
'success_count': successCount,
|
||||
'failure_count': failureCount,
|
||||
'route_weight': routeWeight,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,12 +39,15 @@ class PathRecord {
|
||||
return PathRecord(
|
||||
hopCount: json['hop_count'] as int,
|
||||
tripTimeMs: json['trip_time_ms'] as int,
|
||||
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||
timestamp: json['timestamp'] != null
|
||||
? DateTime.parse(json['timestamp'] as String)
|
||||
: null,
|
||||
wasFloodDiscovery: json['was_flood'] as bool,
|
||||
pathBytes:
|
||||
(json['path_bytes'] as List?)?.map((b) => b as int).toList() ?? [],
|
||||
successCount: json['success_count'] as int? ?? 0,
|
||||
failureCount: json['failure_count'] as int? ?? 0,
|
||||
routeWeight: (json['route_weight'] as num?)?.toDouble() ?? 1.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'contact.dart';
|
||||
|
||||
const int recentAttemptDiversityWindow = 2;
|
||||
|
||||
class PathSelection {
|
||||
final List<int> pathBytes;
|
||||
final int hopCount;
|
||||
@@ -9,3 +15,38 @@ class PathSelection {
|
||||
required this.useFlood,
|
||||
});
|
||||
}
|
||||
|
||||
PathSelection resolvePathSelection(
|
||||
Contact contact, {
|
||||
PathSelection? selection,
|
||||
bool forceFlood = false,
|
||||
}) {
|
||||
if (contact.pathOverride != null) {
|
||||
if (contact.pathOverride! < 0) {
|
||||
return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true);
|
||||
}
|
||||
return PathSelection(
|
||||
pathBytes: contact.pathOverrideBytes ?? Uint8List(0),
|
||||
hopCount: contact.pathOverride!,
|
||||
useFlood: false,
|
||||
);
|
||||
}
|
||||
|
||||
if (forceFlood || contact.pathLength < 0 || selection?.useFlood == true) {
|
||||
return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true);
|
||||
}
|
||||
|
||||
if (selection != null && selection.pathBytes.isNotEmpty) {
|
||||
return PathSelection(
|
||||
pathBytes: selection.pathBytes,
|
||||
hopCount: selection.hopCount,
|
||||
useFlood: false,
|
||||
);
|
||||
}
|
||||
|
||||
return PathSelection(
|
||||
pathBytes: contact.path,
|
||||
hopCount: contact.pathLength,
|
||||
useFlood: false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
enum MessageTranslationStatus { none, pending, completed, failed, skipped }
|
||||
|
||||
extension MessageTranslationStatusValue on MessageTranslationStatus {
|
||||
String get value {
|
||||
switch (this) {
|
||||
case MessageTranslationStatus.pending:
|
||||
return 'pending';
|
||||
case MessageTranslationStatus.completed:
|
||||
return 'completed';
|
||||
case MessageTranslationStatus.failed:
|
||||
return 'failed';
|
||||
case MessageTranslationStatus.skipped:
|
||||
return 'skipped';
|
||||
case MessageTranslationStatus.none:
|
||||
return 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageTranslationStatus parseMessageTranslationStatus(dynamic value) {
|
||||
if (value is! String) {
|
||||
return MessageTranslationStatus.none;
|
||||
}
|
||||
for (final status in MessageTranslationStatus.values) {
|
||||
if (status.value == value) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
return MessageTranslationStatus.none;
|
||||
}
|
||||
|
||||
class TranslationModelRecord {
|
||||
final String id;
|
||||
final String name;
|
||||
final String sourceUrl;
|
||||
final String localPath;
|
||||
final DateTime downloadedAt;
|
||||
final int fileSizeBytes;
|
||||
|
||||
const TranslationModelRecord({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.sourceUrl,
|
||||
required this.localPath,
|
||||
required this.downloadedAt,
|
||||
required this.fileSizeBytes,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'source_url': sourceUrl,
|
||||
'local_path': localPath,
|
||||
'downloaded_at': downloadedAt.millisecondsSinceEpoch,
|
||||
'file_size_bytes': fileSizeBytes,
|
||||
};
|
||||
}
|
||||
|
||||
factory TranslationModelRecord.fromJson(Map<String, dynamic> json) {
|
||||
return TranslationModelRecord(
|
||||
id: json['id'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
sourceUrl: json['source_url'] as String? ?? '',
|
||||
localPath: json['local_path'] as String? ?? '',
|
||||
downloadedAt: DateTime.fromMillisecondsSinceEpoch(
|
||||
json['downloaded_at'] as int? ?? 0,
|
||||
),
|
||||
fileSizeBytes: json['file_size_bytes'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String translationModelFriendlyName(TranslationModelRecord model) {
|
||||
switch (model.id) {
|
||||
case 'hy-mt1.5-1.8b-q4_k_m':
|
||||
return 'Tencent HY-MT 1.5 1.8B Q4_K_M';
|
||||
case 'hy-mt1.5-1.8b-q6_k':
|
||||
return 'Tencent HY-MT 1.5 1.8B Q6_K';
|
||||
default:
|
||||
final trimmed = model.name.trim();
|
||||
if (trimmed.endsWith('.gguf')) {
|
||||
return trimmed.substring(0, trimmed.length - 5);
|
||||
}
|
||||
return trimmed.isEmpty ? model.id : trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
class TranslationLanguageOption {
|
||||
final String code;
|
||||
final String label;
|
||||
|
||||
const TranslationLanguageOption({required this.code, required this.label});
|
||||
}
|
||||
|
||||
const List<TranslationLanguageOption> supportedTranslationLanguages = [
|
||||
TranslationLanguageOption(code: 'bg', label: 'Bulgarian'),
|
||||
TranslationLanguageOption(code: 'de', label: 'German'),
|
||||
TranslationLanguageOption(code: 'en', label: 'English'),
|
||||
TranslationLanguageOption(code: 'es', label: 'Spanish'),
|
||||
TranslationLanguageOption(code: 'fr', label: 'French'),
|
||||
TranslationLanguageOption(code: 'hu', label: 'Hungarian'),
|
||||
TranslationLanguageOption(code: 'it', label: 'Italian'),
|
||||
TranslationLanguageOption(code: 'ja', label: 'Japanese'),
|
||||
TranslationLanguageOption(code: 'ko', label: 'Korean'),
|
||||
TranslationLanguageOption(code: 'nl', label: 'Dutch'),
|
||||
TranslationLanguageOption(code: 'pl', label: 'Polish'),
|
||||
TranslationLanguageOption(code: 'pt', label: 'Portuguese'),
|
||||
TranslationLanguageOption(code: 'ru', label: 'Russian'),
|
||||
TranslationLanguageOption(code: 'sk', label: 'Slovak'),
|
||||
TranslationLanguageOption(code: 'sl', label: 'Slovenian'),
|
||||
TranslationLanguageOption(code: 'sv', label: 'Swedish'),
|
||||
TranslationLanguageOption(code: 'uk', label: 'Ukrainian'),
|
||||
TranslationLanguageOption(code: 'zh', label: 'Chinese'),
|
||||
];
|
||||
|
||||
final List<TranslationModelRecord> translationPresetModels = [
|
||||
TranslationModelRecord(
|
||||
id: 'hy-mt1.5-1.8b-q4_k_m',
|
||||
name: 'HY-MT1.5-1.8B-Q4_K_M.gguf',
|
||||
sourceUrl:
|
||||
'https://huggingface.co/tencent/HY-MT1.5-1.8B-GGUF/resolve/main/HY-MT1.5-1.8B-Q4_K_M.gguf?download=true',
|
||||
localPath: '',
|
||||
downloadedAt: DateTime.fromMillisecondsSinceEpoch(0),
|
||||
fileSizeBytes: 0,
|
||||
),
|
||||
TranslationModelRecord(
|
||||
id: 'hy-mt1.5-1.8b-q6_k',
|
||||
name: 'HY-MT1.5-1.8B-Q6_K.gguf',
|
||||
sourceUrl:
|
||||
'https://huggingface.co/tencent/HY-MT1.5-1.8B-GGUF/resolve/main/HY-MT1.5-1.8B-Q6_K.gguf?download=true',
|
||||
localPath: '',
|
||||
downloadedAt: DateTime.fromMillisecondsSinceEpoch(0),
|
||||
fileSizeBytes: 0,
|
||||
),
|
||||
];
|
||||
@@ -5,6 +5,7 @@ import 'package:provider/provider.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../services/app_debug_log_service.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
class AppDebugLogScreen extends StatelessWidget {
|
||||
const AppDebugLogScreen({super.key});
|
||||
@@ -34,8 +35,9 @@ class AppDebugLogScreen extends StatelessWidget {
|
||||
.join('\n');
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.debugLog_copied)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.debugLog_copied),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/app_settings.dart';
|
||||
import '../models/translation_support.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/notification_service.dart';
|
||||
import '../services/translation_service.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'map_cache_screen.dart';
|
||||
|
||||
class AppSettingsScreen extends StatelessWidget {
|
||||
@@ -21,26 +25,46 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Consumer2<AppSettingsService, MeshCoreConnector>(
|
||||
builder: (context, settingsService, connector, child) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_buildAppearanceCard(context, settingsService),
|
||||
const SizedBox(height: 16),
|
||||
_buildNotificationsCard(context, settingsService),
|
||||
const SizedBox(height: 16),
|
||||
_buildMessagingCard(context, settingsService),
|
||||
const SizedBox(height: 16),
|
||||
_buildBatteryCard(context, settingsService, connector),
|
||||
const SizedBox(height: 16),
|
||||
_buildMapSettingsCard(context, settingsService),
|
||||
const SizedBox(height: 16),
|
||||
_buildDebugCard(context, settingsService),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
child:
|
||||
Consumer3<
|
||||
AppSettingsService,
|
||||
MeshCoreConnector,
|
||||
TranslationService
|
||||
>(
|
||||
builder:
|
||||
(
|
||||
context,
|
||||
settingsService,
|
||||
connector,
|
||||
translationService,
|
||||
child,
|
||||
) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_buildAppearanceCard(context, settingsService),
|
||||
const SizedBox(height: 16),
|
||||
_buildNotificationsCard(context, settingsService),
|
||||
const SizedBox(height: 16),
|
||||
_buildMessagingCard(context, settingsService),
|
||||
const SizedBox(height: 16),
|
||||
if (!kIsWeb) ...[
|
||||
_buildTranslationCard(
|
||||
context,
|
||||
settingsService,
|
||||
translationService,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
_buildBatteryCard(context, settingsService, connector),
|
||||
const SizedBox(height: 16),
|
||||
_buildMapSettingsCard(context, settingsService),
|
||||
const SizedBox(height: 16),
|
||||
_buildDebugCard(context, settingsService),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -128,13 +152,12 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
.requestPermissions();
|
||||
if (!granted) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.appSettings_notificationPermissionDenied,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.appSettings_notificationPermissionDenied,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
}
|
||||
return;
|
||||
@@ -143,15 +166,14 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
|
||||
await settingsService.setNotificationsEnabled(value);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
value
|
||||
? context.l10n.appSettings_notificationsEnabled
|
||||
: context.l10n.appSettings_notificationsDisabled,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
value
|
||||
? context.l10n.appSettings_notificationsEnabled
|
||||
: context.l10n.appSettings_notificationsDisabled,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -278,19 +300,26 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
value: settingsService.settings.clearPathOnMaxRetry,
|
||||
onChanged: (value) {
|
||||
settingsService.setClearPathOnMaxRetry(value);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
value
|
||||
? context.l10n.appSettings_pathsWillBeCleared
|
||||
: context.l10n.appSettings_pathsWillNotBeCleared,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
value
|
||||
? context.l10n.appSettings_pathsWillBeCleared
|
||||
: context.l10n.appSettings_pathsWillNotBeCleared,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
},
|
||||
),
|
||||
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),
|
||||
@@ -298,18 +327,129 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
value: settingsService.settings.autoRouteRotationEnabled,
|
||||
onChanged: (value) {
|
||||
settingsService.setAutoRouteRotationEnabled(value);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
value
|
||||
? context.l10n.appSettings_autoRouteRotationEnabled
|
||||
: context.l10n.appSettings_autoRouteRotationDisabled,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
value
|
||||
? context.l10n.appSettings_autoRouteRotationEnabled
|
||||
: context.l10n.appSettings_autoRouteRotationDisabled,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (settingsService.settings.autoRouteRotationEnabled) ...[
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
title: Text(context.l10n.appSettings_maxRouteWeight),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(context.l10n.appSettings_maxRouteWeightSubtitle),
|
||||
Slider(
|
||||
value: settingsService.settings.maxRouteWeight,
|
||||
min: 1,
|
||||
max: 10,
|
||||
divisions: 9,
|
||||
label: settingsService.settings.maxRouteWeight
|
||||
.round()
|
||||
.toString(),
|
||||
onChanged: (value) =>
|
||||
settingsService.setMaxRouteWeight(value),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
title: Text(context.l10n.appSettings_initialRouteWeight),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(context.l10n.appSettings_initialRouteWeightSubtitle),
|
||||
Slider(
|
||||
value: settingsService.settings.initialRouteWeight,
|
||||
min: 0.5,
|
||||
max: 5.0,
|
||||
divisions: 9,
|
||||
label: settingsService.settings.initialRouteWeight
|
||||
.toStringAsFixed(1),
|
||||
onChanged: (value) =>
|
||||
settingsService.setInitialRouteWeight(value),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
title: Text(context.l10n.appSettings_routeWeightSuccessIncrement),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context
|
||||
.l10n
|
||||
.appSettings_routeWeightSuccessIncrementSubtitle,
|
||||
),
|
||||
Slider(
|
||||
value: settingsService.settings.routeWeightSuccessIncrement,
|
||||
min: 0.1,
|
||||
max: 2.0,
|
||||
divisions: 19,
|
||||
label: settingsService.settings.routeWeightSuccessIncrement
|
||||
.toStringAsFixed(1),
|
||||
onChanged: (value) =>
|
||||
settingsService.setRouteWeightSuccessIncrement(value),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
title: Text(context.l10n.appSettings_routeWeightFailureDecrement),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context
|
||||
.l10n
|
||||
.appSettings_routeWeightFailureDecrementSubtitle,
|
||||
),
|
||||
Slider(
|
||||
value: settingsService.settings.routeWeightFailureDecrement,
|
||||
min: 0.1,
|
||||
max: 2.0,
|
||||
divisions: 19,
|
||||
label: settingsService.settings.routeWeightFailureDecrement
|
||||
.toStringAsFixed(1),
|
||||
onChanged: (value) =>
|
||||
settingsService.setRouteWeightFailureDecrement(value),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
title: Text(context.l10n.appSettings_maxMessageRetries),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(context.l10n.appSettings_maxMessageRetriesSubtitle),
|
||||
Slider(
|
||||
value: settingsService.settings.maxMessageRetries
|
||||
.toDouble(),
|
||||
min: 2,
|
||||
max: 10,
|
||||
divisions: 8,
|
||||
label: settingsService.settings.maxMessageRetries
|
||||
.toString(),
|
||||
onChanged: (value) =>
|
||||
settingsService.setMaxMessageRetries(value.toInt()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -410,6 +550,211 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTranslationCard(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
TranslationService translationService,
|
||||
) {
|
||||
final settings = settingsService.settings;
|
||||
return Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
child: Text(
|
||||
context.l10n.translation_title,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.translate),
|
||||
title: Text(context.l10n.translation_enableTitle),
|
||||
subtitle: Text(context.l10n.translation_enableSubtitle),
|
||||
value: settings.translationEnabled,
|
||||
onChanged: settingsService.setTranslationEnabled,
|
||||
),
|
||||
const Divider(height: 1),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.outgoing_mail),
|
||||
title: Text(context.l10n.translation_composerTitle),
|
||||
subtitle: Text(context.l10n.translation_composerSubtitle),
|
||||
value: settings.composerTranslationEnabled,
|
||||
onChanged: settingsService.setComposerTranslationEnabled,
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.language),
|
||||
title: Text(context.l10n.translation_targetLanguage),
|
||||
subtitle: Text(
|
||||
_translationLanguageLabel(
|
||||
context,
|
||||
settings.translationTargetLanguageCode,
|
||||
),
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () =>
|
||||
_showTranslationLanguageDialog(context, settingsService),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: DropdownButtonFormField<String>(
|
||||
initialValue: settings.translationSelectedModelId,
|
||||
isExpanded: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: context.l10n.translation_downloadedModelLabel,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
for (final model in settings.translationDownloadedModels)
|
||||
DropdownMenuItem(
|
||||
value: model.id,
|
||||
child: Text(translationModelFriendlyName(model)),
|
||||
),
|
||||
],
|
||||
onChanged: settings.translationDownloadedModels.isEmpty
|
||||
? null
|
||||
: (value) {
|
||||
settingsService.setTranslationSelectedModelId(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: DropdownButtonFormField<String>(
|
||||
initialValue: null,
|
||||
isExpanded: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: context.l10n.translation_presetModelLabel,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
for (final preset in translationPresetModels)
|
||||
DropdownMenuItem(
|
||||
value: preset.sourceUrl,
|
||||
child: Text(translationModelFriendlyName(preset)),
|
||||
),
|
||||
],
|
||||
onChanged: translationService.isBusy
|
||||
? null
|
||||
: (value) async {
|
||||
if (value == null) return;
|
||||
final preset = translationPresetModels.firstWhere(
|
||||
(entry) => entry.sourceUrl == value,
|
||||
);
|
||||
await _downloadTranslationModel(
|
||||
context,
|
||||
translationService,
|
||||
settingsService,
|
||||
sourceUrl: preset.sourceUrl,
|
||||
fileName: preset.name,
|
||||
id: preset.id,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
|
||||
child: Column(
|
||||
children: [
|
||||
_TranslationUrlField(
|
||||
initialValue: settings.translationModelSourceUrl ?? '',
|
||||
onChanged: settingsService.setTranslationModelSourceUrl,
|
||||
onDownload: translationService.isBusy
|
||||
? null
|
||||
: (url) => _downloadTranslationModel(
|
||||
context,
|
||||
translationService,
|
||||
settingsService,
|
||||
sourceUrl: url,
|
||||
),
|
||||
downloadLabel: translationService.isDownloading
|
||||
? context.l10n.translation_downloading
|
||||
: translationService.isBusy
|
||||
? context.l10n.translation_working
|
||||
: context.l10n.translation_downloadModel,
|
||||
isDownloading: translationService.isDownloading,
|
||||
onCancel: translationService.cancelDownload,
|
||||
labelText: context.l10n.translation_manualUrlLabel,
|
||||
stopLabel: context.l10n.translation_stop,
|
||||
),
|
||||
if (translationService.isDownloading) ...[
|
||||
const SizedBox(height: 12),
|
||||
LinearProgressIndicator(
|
||||
value:
|
||||
translationService.downloadFileName ==
|
||||
'Merging chunks...'
|
||||
? null
|
||||
: translationService.downloadProgress,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
_downloadProgressLabel(context, translationService),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (settings.translationDownloadedModels.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
context.l10n.translation_downloadedModels,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
for (final model in settings.translationDownloadedModels)
|
||||
Card.outlined(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
leading: Icon(
|
||||
model.id == settings.translationSelectedModelId
|
||||
? Icons.check_circle
|
||||
: Icons.memory_outlined,
|
||||
),
|
||||
title: Text(translationModelFriendlyName(model)),
|
||||
subtitle: Text(_downloadedModelLabel(model)),
|
||||
trailing: IconButton(
|
||||
tooltip: context.l10n.translation_deleteModel,
|
||||
onPressed: translationService.isBusy
|
||||
? null
|
||||
: () => _deleteTranslationModel(
|
||||
context,
|
||||
translationService,
|
||||
model,
|
||||
),
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
),
|
||||
onTap: () => settingsService
|
||||
.setTranslationSelectedModelId(model.id),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (translationService.lastError != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
translationService.lastError!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Fixed rendering issues
|
||||
Widget _buildBatteryCard(
|
||||
BuildContext context,
|
||||
@@ -577,6 +922,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;
|
||||
}
|
||||
@@ -664,6 +1015,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',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -699,25 +1062,25 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
children: [
|
||||
Text(context.l10n.appSettings_showNodesDiscoveredWithin),
|
||||
const SizedBox(height: 16),
|
||||
ListTile(
|
||||
RadioListTile<double>(
|
||||
title: Text(context.l10n.appSettings_allTime),
|
||||
leading: Radio<double>(value: 0),
|
||||
value: 0,
|
||||
),
|
||||
ListTile(
|
||||
RadioListTile<double>(
|
||||
title: Text(context.l10n.appSettings_lastHour),
|
||||
leading: Radio<double>(value: 1),
|
||||
value: 1,
|
||||
),
|
||||
ListTile(
|
||||
RadioListTile<double>(
|
||||
title: Text(context.l10n.appSettings_last6Hours),
|
||||
leading: Radio<double>(value: 6),
|
||||
value: 6,
|
||||
),
|
||||
ListTile(
|
||||
RadioListTile<double>(
|
||||
title: Text(context.l10n.appSettings_last24Hours),
|
||||
leading: Radio<double>(value: 24),
|
||||
value: 24,
|
||||
),
|
||||
ListTile(
|
||||
RadioListTile<double>(
|
||||
title: Text(context.l10n.appSettings_lastWeek),
|
||||
leading: Radio<double>(value: 168),
|
||||
value: 168,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -751,13 +1114,13 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
RadioListTile<UnitSystem>(
|
||||
title: Text(context.l10n.appSettings_unitsMetric),
|
||||
leading: const Radio<UnitSystem>(value: UnitSystem.metric),
|
||||
value: UnitSystem.metric,
|
||||
),
|
||||
ListTile(
|
||||
RadioListTile<UnitSystem>(
|
||||
title: Text(context.l10n.appSettings_unitsImperial),
|
||||
leading: const Radio<UnitSystem>(value: UnitSystem.imperial),
|
||||
value: UnitSystem.imperial,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -772,6 +1135,126 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _showTranslationLanguageDialog(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => _TranslationLanguageDialogContent(
|
||||
currentLanguageCode:
|
||||
settingsService.settings.translationTargetLanguageCode,
|
||||
onLanguageSelected: (value) {
|
||||
settingsService.setTranslationTargetLanguageCode(value);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _downloadTranslationModel(
|
||||
BuildContext context,
|
||||
TranslationService translationService,
|
||||
AppSettingsService settingsService, {
|
||||
required String sourceUrl,
|
||||
String? fileName,
|
||||
String? id,
|
||||
}) async {
|
||||
if (sourceUrl.isEmpty) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.translation_enterUrlFirst),
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await translationService.downloadModel(
|
||||
sourceUrl: sourceUrl,
|
||||
fileName: fileName,
|
||||
id: id,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.translation_modelDownloaded),
|
||||
);
|
||||
await settingsService.setTranslationEnabled(true);
|
||||
} on TranslationDownloadCancelled {
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.translation_downloadStopped),
|
||||
);
|
||||
} catch (error) {
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.translation_downloadFailed(error.toString()),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _translationLanguageLabel(BuildContext context, String? languageCode) {
|
||||
if (languageCode == null || languageCode.isEmpty) {
|
||||
return context.l10n.translation_useAppLanguage;
|
||||
}
|
||||
for (final option in supportedTranslationLanguages) {
|
||||
if (option.code == languageCode) {
|
||||
return option.label;
|
||||
}
|
||||
}
|
||||
return languageCode.toUpperCase();
|
||||
}
|
||||
|
||||
String _downloadProgressLabel(
|
||||
BuildContext context,
|
||||
TranslationService translationService,
|
||||
) {
|
||||
final fileName = translationService.downloadFileName ?? 'Model';
|
||||
if (fileName == 'Merging chunks...') {
|
||||
return context.l10n.translation_mergingChunks;
|
||||
}
|
||||
final currentMb = translationService.downloadedBytes / (1024 * 1024);
|
||||
final totalBytes = translationService.downloadTotalBytes;
|
||||
if (totalBytes == null || totalBytes <= 0) {
|
||||
return '$fileName: ${currentMb.toStringAsFixed(1)} MB';
|
||||
}
|
||||
final totalMb = totalBytes / (1024 * 1024);
|
||||
final percent = ((translationService.downloadProgress ?? 0) * 100)
|
||||
.toStringAsFixed(0);
|
||||
return '$fileName: ${currentMb.toStringAsFixed(1)} / ${totalMb.toStringAsFixed(1)} MB ($percent%)';
|
||||
}
|
||||
|
||||
Future<void> _deleteTranslationModel(
|
||||
BuildContext context,
|
||||
TranslationService translationService,
|
||||
TranslationModelRecord model,
|
||||
) async {
|
||||
try {
|
||||
await translationService.removeModel(model);
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
// TODO: l10n
|
||||
content: Text('Deleted ${translationModelFriendlyName(model)}.'),
|
||||
);
|
||||
} catch (error) {
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text('Delete failed: $error'),
|
||||
); // TODO: l10n
|
||||
}
|
||||
}
|
||||
|
||||
String _downloadedModelLabel(TranslationModelRecord model) {
|
||||
final sizeMb = model.fileSizeBytes / (1024 * 1024);
|
||||
final source = model.sourceUrl.isEmpty ? model.name : model.sourceUrl;
|
||||
return '${sizeMb.toStringAsFixed(1)} MB • $source';
|
||||
}
|
||||
|
||||
Widget _buildDebugCard(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
@@ -795,15 +1278,14 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
onChanged: (value) async {
|
||||
await settingsService.setAppDebugLogEnabled(value);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
value
|
||||
? context.l10n.appSettings_appDebugLoggingEnabled
|
||||
: context.l10n.appSettings_appDebugLoggingDisabled,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
value
|
||||
? context.l10n.appSettings_appDebugLoggingEnabled
|
||||
: context.l10n.appSettings_appDebugLoggingDisabled,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -812,3 +1294,179 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Owns the [TextEditingController] for the manual model URL field so it
|
||||
/// survives rebuilds of the parent [Consumer3].
|
||||
class _TranslationUrlField extends StatefulWidget {
|
||||
const _TranslationUrlField({
|
||||
required this.initialValue,
|
||||
required this.onChanged,
|
||||
required this.onDownload,
|
||||
required this.downloadLabel,
|
||||
required this.isDownloading,
|
||||
required this.onCancel,
|
||||
required this.labelText,
|
||||
required this.stopLabel,
|
||||
});
|
||||
|
||||
final String initialValue;
|
||||
final ValueChanged<String> onChanged;
|
||||
final void Function(String url)? onDownload;
|
||||
final String downloadLabel;
|
||||
final bool isDownloading;
|
||||
final VoidCallback onCancel;
|
||||
final String labelText;
|
||||
final String stopLabel;
|
||||
|
||||
@override
|
||||
State<_TranslationUrlField> createState() => _TranslationUrlFieldState();
|
||||
}
|
||||
|
||||
class _TranslationUrlFieldState extends State<_TranslationUrlField> {
|
||||
late final TextEditingController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController(text: widget.initialValue);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: _controller,
|
||||
decoration: InputDecoration(
|
||||
labelText: widget.labelText,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
onChanged: widget.onChanged,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FilledButton.icon(
|
||||
onPressed: widget.onDownload == null
|
||||
? null
|
||||
: () => widget.onDownload!(_controller.text.trim()),
|
||||
icon: const Icon(Icons.download),
|
||||
label: Text(widget.downloadLabel),
|
||||
),
|
||||
),
|
||||
if (widget.isDownloading) ...[
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: widget.onCancel,
|
||||
icon: const Icon(Icons.stop_circle_outlined),
|
||||
label: Text(widget.stopLabel),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Dialog content for choosing the translation target language.
|
||||
/// Owns the search [TextEditingController] so it is properly disposed.
|
||||
class _TranslationLanguageDialogContent extends StatefulWidget {
|
||||
const _TranslationLanguageDialogContent({
|
||||
required this.currentLanguageCode,
|
||||
required this.onLanguageSelected,
|
||||
});
|
||||
|
||||
final String? currentLanguageCode;
|
||||
final ValueChanged<String?> onLanguageSelected;
|
||||
|
||||
@override
|
||||
State<_TranslationLanguageDialogContent> createState() =>
|
||||
_TranslationLanguageDialogContentState();
|
||||
}
|
||||
|
||||
class _TranslationLanguageDialogContentState
|
||||
extends State<_TranslationLanguageDialogContent> {
|
||||
late final TextEditingController _searchController;
|
||||
List<TranslationLanguageOption> _filtered = supportedTranslationLanguages;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController = TextEditingController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(context.l10n.translation_targetLanguage),
|
||||
content: SizedBox(
|
||||
width: 360,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _searchController,
|
||||
decoration: const InputDecoration(
|
||||
prefixIcon: Icon(Icons.search),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (value) {
|
||||
final normalized = value.trim().toLowerCase();
|
||||
setState(() {
|
||||
_filtered = supportedTranslationLanguages.where((option) {
|
||||
return option.label.toLowerCase().contains(normalized) ||
|
||||
option.code.toLowerCase().contains(normalized);
|
||||
}).toList();
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Flexible(
|
||||
child: RadioGroup<String?>(
|
||||
groupValue: widget.currentLanguageCode,
|
||||
onChanged: (value) {
|
||||
widget.onLanguageSelected(value);
|
||||
},
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
RadioListTile<String?>(
|
||||
value: null,
|
||||
title: Text(context.l10n.translation_useAppLanguage),
|
||||
),
|
||||
for (final option in _filtered)
|
||||
RadioListTile<String?>(
|
||||
value: option.code,
|
||||
title: Text(option.label),
|
||||
subtitle: Text(option.code.toUpperCase()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(context.l10n.common_close),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import '../l10n/l10n.dart';
|
||||
import '../services/ble_debug_log_service.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
enum _BleLogView { frames, rawLogRx }
|
||||
|
||||
@@ -52,10 +53,9 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
.join('\n');
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.debugLog_bleCopied),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.debugLog_bleCopied),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
@@ -118,6 +118,19 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
: Icons.download,
|
||||
size: 18,
|
||||
),
|
||||
onLongPress: () async {
|
||||
await Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: entry.payload
|
||||
.map(
|
||||
(b) => b
|
||||
.toRadixString(16)
|
||||
.padLeft(2, '0'),
|
||||
)
|
||||
.join(''),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -270,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) {
|
||||
|
||||
@@ -4,28 +4,34 @@ import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../utils/platform_info.dart';
|
||||
import '../helpers/chat_scroll_controller.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../helpers/link_handler.dart';
|
||||
import '../helpers/gif_helper.dart';
|
||||
import '../helpers/reaction_helper.dart';
|
||||
import '../helpers/utf8_length_limiter.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/channel.dart';
|
||||
import '../models/channel_message.dart';
|
||||
import '../models/translation_support.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/chat_text_scale_service.dart';
|
||||
import '../services/translation_service.dart';
|
||||
import '../utils/emoji_utils.dart';
|
||||
import '../widgets/chat_zoom_wrapper.dart';
|
||||
import '../widgets/emoji_picker.dart';
|
||||
import '../widgets/gif_message.dart';
|
||||
import '../widgets/jump_to_bottom_button.dart';
|
||||
import '../widgets/gif_picker.dart';
|
||||
import '../widgets/message_translation_button.dart';
|
||||
import '../widgets/message_status_icon.dart';
|
||||
import '../widgets/radio_stats_entry.dart';
|
||||
import '../widgets/translated_message_content.dart';
|
||||
import 'channel_message_path_screen.dart';
|
||||
import 'map_screen.dart';
|
||||
|
||||
@@ -47,6 +53,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
bool _isLoadingOlder = false;
|
||||
|
||||
MeshCoreConnector? _connector;
|
||||
DateTime? _lastChannelSendAt;
|
||||
bool _channelSkipNextBottomSnap = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -55,11 +63,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();
|
||||
@@ -103,11 +145,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
Future<void> _scrollToMessage(String messageId) async {
|
||||
final key = _messageKeys[messageId];
|
||||
if (key == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.chat_originalMessageNotFound),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.chat_originalMessageNotFound),
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -166,6 +207,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 +285,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();
|
||||
});
|
||||
|
||||
@@ -283,8 +356,16 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
final settingsService = context.watch<AppSettingsService>();
|
||||
final enableTracing = settingsService.settings.enableMessageTracing;
|
||||
final isOutgoing = message.isOutgoing;
|
||||
final gifId = _parseGifId(message.text);
|
||||
final gifId = GifHelper.parseGif(message.text);
|
||||
final poi = _parsePoiMessage(message.text);
|
||||
final translatedDisplayText =
|
||||
message.translatedText != null &&
|
||||
message.translatedText!.trim().isNotEmpty
|
||||
? message.translatedText!.trim()
|
||||
: message.text;
|
||||
final originalDisplayText = message.isOutgoing
|
||||
? message.originalText
|
||||
: (translatedDisplayText != message.text ? message.text : null);
|
||||
final displayPath = message.pathBytes.isNotEmpty
|
||||
? message.pathBytes
|
||||
: (message.pathVariants.isNotEmpty
|
||||
@@ -311,8 +392,13 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
],
|
||||
Flexible(
|
||||
child: GestureDetector(
|
||||
onTap: () => _showMessagePathInfo(message),
|
||||
onTap: PlatformInfo.isDesktop
|
||||
? null
|
||||
: () => _showMessagePathInfo(message),
|
||||
onLongPress: () => _showMessageActions(message),
|
||||
onSecondaryTapUp: PlatformInfo.isDesktop
|
||||
? (_) => _showMessageActions(message)
|
||||
: null,
|
||||
child: Container(
|
||||
padding: gifId != null
|
||||
? const EdgeInsets.all(4)
|
||||
@@ -430,24 +516,17 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Linkify(
|
||||
text: message.text,
|
||||
child: TranslatedMessageContent(
|
||||
displayText: translatedDisplayText,
|
||||
originalText: originalDisplayText,
|
||||
style: TextStyle(
|
||||
fontSize: bodyFontSize * textScale,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
originalStyle: TextStyle(
|
||||
fontSize: bodyFontSize * textScale,
|
||||
color: Colors.green,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
options: const LinkifyOptions(
|
||||
humanize: false,
|
||||
defaultToHttps: false,
|
||||
),
|
||||
linkifiers: const [UrlLinkifier()],
|
||||
onOpen: (link) => LinkHandler.handleLinkTap(
|
||||
context,
|
||||
link.url,
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Theme.of(context).colorScheme.onSurface
|
||||
.withValues(alpha: 0.72),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -557,7 +636,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
],
|
||||
);
|
||||
|
||||
if (!isOutgoing) {
|
||||
if (!isOutgoing && !PlatformInfo.isDesktop) {
|
||||
return _SwipeReplyBubble(
|
||||
maxSwipeOffset: maxSwipeOffset,
|
||||
replySwipeThreshold: replySwipeThreshold,
|
||||
@@ -621,7 +700,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final previewTextColor = colorScheme.onSurface.withValues(alpha: 0.7);
|
||||
|
||||
final gifId = _parseGifId(replyText);
|
||||
final gifId = GifHelper.parseGif(replyText);
|
||||
final poi = _parsePoiMessage(replyText);
|
||||
|
||||
Widget contentPreview;
|
||||
@@ -733,12 +812,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
String? _parseGifId(String text) {
|
||||
final trimmed = text.trim();
|
||||
final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed);
|
||||
return match?.group(1);
|
||||
}
|
||||
|
||||
_PoiInfo? _parsePoiMessage(String text) {
|
||||
final trimmed = text.trim();
|
||||
final match = RegExp(
|
||||
@@ -819,7 +892,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
isScrollControlled: true,
|
||||
builder: (context) => GifPicker(
|
||||
onGifSelected: (gifId) {
|
||||
_textController.text = 'g:$gifId';
|
||||
_textController.text = GifHelper.encodeGif(gifId);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -933,6 +1006,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
Widget _buildMessageComposer() {
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
final maxBytes = maxChannelMessageBytes(connector.selfName);
|
||||
final settings = context.watch<AppSettingsService>().settings;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -964,11 +1038,17 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
onPressed: () => _showGifPicker(context),
|
||||
tooltip: context.l10n.chat_sendGif,
|
||||
),
|
||||
if (settings.translationEnabled)
|
||||
MessageTranslationButton(
|
||||
enabled: settings.composerTranslationEnabled,
|
||||
languageCode: settings.translationTargetLanguageCode,
|
||||
onPressed: _showTranslationOptions,
|
||||
),
|
||||
Expanded(
|
||||
child: ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: _textController,
|
||||
builder: (context, value, child) {
|
||||
final gifId = _parseGifId(value.text);
|
||||
final gifId = GifHelper.parseGif(value.text);
|
||||
if (gifId != null) {
|
||||
return Focus(
|
||||
autofocus: true,
|
||||
@@ -1041,6 +1121,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.send),
|
||||
tooltip: context.l10n.chat_sendMessage,
|
||||
onPressed: _sendMessage,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
@@ -1051,29 +1132,87 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _sendMessage() {
|
||||
Future<void> _showTranslationOptions() async {
|
||||
final settingsService = context.read<AppSettingsService>();
|
||||
final settings = settingsService.settings;
|
||||
await showMessageTranslationSheet(
|
||||
context: context,
|
||||
enabled: settings.composerTranslationEnabled,
|
||||
selectedLanguageCode: settings.translationTargetLanguageCode,
|
||||
onEnabledChanged: settingsService.setComposerTranslationEnabled,
|
||||
onLanguageSelected: settingsService.setTranslationTargetLanguageCode,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _sendMessage() async {
|
||||
final text = _textController.text.trim();
|
||||
if (text.isEmpty) return;
|
||||
|
||||
final now = DateTime.now();
|
||||
if (_lastChannelSendAt != null &&
|
||||
now.difference(_lastChannelSendAt!) < const Duration(seconds: 1)) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.chat_sendCooldown),
|
||||
);
|
||||
return;
|
||||
}
|
||||
_lastChannelSendAt = now;
|
||||
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final settings = context.read<AppSettingsService>().settings;
|
||||
final translationService = context.read<TranslationService>();
|
||||
|
||||
String messageText = text;
|
||||
String? originalText;
|
||||
String? translatedLanguageCode;
|
||||
String? translationModelId;
|
||||
if (settings.translationEnabled) {
|
||||
final targetLanguageCode = translationService.resolvedTargetLanguageCode(
|
||||
Localizations.localeOf(context).languageCode,
|
||||
);
|
||||
if (translationService.shouldTranslateOutgoing(
|
||||
text: text,
|
||||
targetLanguageCode: targetLanguageCode,
|
||||
)) {
|
||||
final result = await translationService.translateOutgoingText(
|
||||
text: text,
|
||||
targetLanguageCode: targetLanguageCode,
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (result != null &&
|
||||
result.status == MessageTranslationStatus.completed &&
|
||||
result.translatedText.isNotEmpty) {
|
||||
messageText = result.translatedText;
|
||||
originalText = text;
|
||||
translatedLanguageCode = result.targetLanguageCode;
|
||||
translationModelId = result.modelId;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_replyingToMessage != null) {
|
||||
messageText = '@[${_replyingToMessage!.senderName}] $text';
|
||||
messageText = '@[${_replyingToMessage!.senderName}] $messageText';
|
||||
}
|
||||
|
||||
final maxBytes = maxChannelMessageBytes(connector.selfName);
|
||||
if (utf8.encode(messageText).length > maxBytes) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.chat_messageTooLong(maxBytes))),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.chat_messageTooLong(maxBytes)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
connector.sendChannelMessage(widget.channel, messageText);
|
||||
_textController.clear();
|
||||
_cancelReply();
|
||||
_textFieldFocusNode.requestFocus();
|
||||
connector.sendChannelMessage(
|
||||
widget.channel,
|
||||
messageText,
|
||||
originalText: originalText,
|
||||
translatedLanguageCode: translatedLanguageCode,
|
||||
translationModelId: translationModelId,
|
||||
);
|
||||
}
|
||||
|
||||
String _formatTime(DateTime time) {
|
||||
@@ -1112,6 +1251,15 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
_setReplyingTo(message);
|
||||
},
|
||||
),
|
||||
if (PlatformInfo.isDesktop)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.route),
|
||||
title: Text(context.l10n.chat_path),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
_showMessagePathInfo(message);
|
||||
},
|
||||
),
|
||||
// Can't react to your own messages
|
||||
if (!message.isOutgoing)
|
||||
ListTile(
|
||||
@@ -1171,23 +1319,25 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
message.senderName,
|
||||
message.text,
|
||||
);
|
||||
final reactionText = 'r:$hash:$emojiIndex';
|
||||
final reactionText = ReactionHelper.encodeReaction(hash, emojiIndex);
|
||||
connector.sendChannelMessage(widget.channel, reactionText);
|
||||
}
|
||||
|
||||
void _copyMessageText(String text) {
|
||||
Clipboard.setData(ClipboardData(text: text));
|
||||
ScaffoldMessenger.of(
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageCopied)));
|
||||
content: Text(context.l10n.chat_messageCopied),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _deleteMessage(ChannelMessage message) async {
|
||||
await context.read<MeshCoreConnector>().deleteChannelMessage(message);
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageDeleted)));
|
||||
content: Text(context.l10n.chat_messageDeleted),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatPathPrefixes(Uint8List pathBytes) {
|
||||
|
||||
@@ -40,8 +40,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
final primaryPath = !channelMessage && !message.isOutgoing
|
||||
? Uint8List.fromList(primaryPathTmp.reversed.toList())
|
||||
: primaryPathTmp;
|
||||
|
||||
final hops = _buildPathHops(primaryPath, connector.contacts, l10n);
|
||||
final hops = _buildPathHops(primaryPath, connector, l10n);
|
||||
final hasHopDetails = primaryPath.isNotEmpty;
|
||||
final observedLabel = _formatObservedHops(
|
||||
primaryPath.length,
|
||||
@@ -62,8 +61,12 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
builder: (context) => PathTraceMapScreen(
|
||||
title: context.l10n.contacts_repeaterPathTrace,
|
||||
path: primaryPath,
|
||||
flipPathRound: true,
|
||||
reversePathRound: !message.isOutgoing && !channelMessage,
|
||||
flipPathAround: true,
|
||||
reversePathAround:
|
||||
!(!channelMessage && !message.isOutgoing),
|
||||
pathHashByteWidth: context
|
||||
.read<MeshCoreConnector>()
|
||||
.pathHashByteWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -302,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() {
|
||||
@@ -336,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>(
|
||||
@@ -364,11 +385,7 @@ class _ChannelMessagePathMapScreenState
|
||||
: selectedPathTmp;
|
||||
|
||||
final selectedIndex = _indexForPath(selectedPath, observedPaths);
|
||||
final hops = _buildPathHops(
|
||||
selectedPath,
|
||||
connector.contacts,
|
||||
context.l10n,
|
||||
);
|
||||
final hops = _buildPathHops(selectedPath, connector, context.l10n);
|
||||
|
||||
final points = <LatLng>[];
|
||||
|
||||
@@ -423,6 +440,7 @@ class _ChannelMessagePathMapScreenState
|
||||
children: [
|
||||
FlutterMap(
|
||||
key: mapKey,
|
||||
mapController: _mapController,
|
||||
options: MapOptions(
|
||||
initialCenter: initialCenter,
|
||||
initialZoom: initialZoom,
|
||||
@@ -474,6 +492,7 @@ class _ChannelMessagePathMapScreenState
|
||||
) {
|
||||
setState(() {
|
||||
_selectedPath = observedPaths[index].pathBytes;
|
||||
_focusedHopIndex = null;
|
||||
});
|
||||
}),
|
||||
if (points.isEmpty)
|
||||
@@ -729,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(
|
||||
@@ -789,19 +817,83 @@ class _ObservedPath {
|
||||
|
||||
List<_PathHop> _buildPathHops(
|
||||
Uint8List pathBytes,
|
||||
List<Contact> contacts,
|
||||
MeshCoreConnector connector,
|
||||
AppLocalizations l10n,
|
||||
) {
|
||||
if (pathBytes.isEmpty) return const [];
|
||||
final candidatesByPrefix = <int, List<Contact>>{};
|
||||
final allContacts = connector.allContacts;
|
||||
for (final contact in 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();
|
||||
var lastDistance = 0.0;
|
||||
var bestDistance = 0.0;
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
// If the best candidate is much farther than the previous hop, it's likely not the correct match.
|
||||
if (lastDistance + bestDistance > 50000 &&
|
||||
candidates != null &&
|
||||
candidates.isNotEmpty) {
|
||||
i--;
|
||||
lastDistance = bestDistance;
|
||||
continue;
|
||||
}
|
||||
lastDistance = bestDistance;
|
||||
|
||||
hops.add(
|
||||
_PathHop(
|
||||
index: i + 1,
|
||||
prefix: prefix,
|
||||
prefix: pathBytes[i],
|
||||
contact: contact,
|
||||
position: _resolvePosition(contact),
|
||||
position: resolvedPosition,
|
||||
l10n: l10n,
|
||||
),
|
||||
);
|
||||
@@ -809,42 +901,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) {
|
||||
|
||||
+242
-241
@@ -4,6 +4,7 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:meshcore_open/storage/channel_message_store.dart';
|
||||
import 'package:meshcore_open/utils/platform_info.dart';
|
||||
import 'package:meshcore_open/widgets/app_bar.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
@@ -11,6 +12,7 @@ import 'package:uuid/uuid.dart';
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/ui_view_state_service.dart';
|
||||
import '../models/channel.dart';
|
||||
import '../models/community.dart';
|
||||
import '../storage/community_store.dart';
|
||||
@@ -22,14 +24,13 @@ import '../widgets/empty_state.dart';
|
||||
import '../widgets/qr_code_display.dart';
|
||||
import '../widgets/quick_switch_bar.dart';
|
||||
import '../widgets/unread_badge.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'channel_chat_screen.dart';
|
||||
import 'community_qr_scanner_screen.dart';
|
||||
import 'contacts_screen.dart';
|
||||
import 'map_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
|
||||
enum ChannelSortOption { manual, name, latestMessages, unread }
|
||||
|
||||
class ChannelsScreen extends StatefulWidget {
|
||||
final bool hideBackButton;
|
||||
|
||||
@@ -43,17 +44,20 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
with DisconnectNavigationMixin {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final CommunityStore _communityStore = CommunityStore();
|
||||
String _searchQuery = '';
|
||||
Timer? _searchDebounce;
|
||||
ChannelSortOption _sortOption = ChannelSortOption.manual;
|
||||
List<Community> _communities = [];
|
||||
|
||||
// Cache of PSK hex -> Community for quick lookup
|
||||
final Map<String, Community> _pskToCommunity = {};
|
||||
|
||||
ChannelMessageStore get _channelMessageStore => ChannelMessageStore();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController.text = context
|
||||
.read<UiViewStateService>()
|
||||
.channelsSearchText;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<MeshCoreConnector>().getChannels();
|
||||
_loadCommunities();
|
||||
@@ -61,6 +65,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
}
|
||||
|
||||
Future<void> _loadCommunities() async {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||
final communities = await _communityStore.loadCommunities();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@@ -106,7 +112,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
final viewState = context.watch<UiViewStateService>();
|
||||
|
||||
final channelMessageStore = ChannelMessageStore();
|
||||
channelMessageStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||
|
||||
// Auto-navigate back to scanner if disconnected
|
||||
if (!checkConnectionAndNavigate(connector)) {
|
||||
@@ -199,6 +208,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
final filteredChannels = _filterAndSortChannels(
|
||||
channels,
|
||||
connector,
|
||||
viewState,
|
||||
);
|
||||
|
||||
return Column(
|
||||
@@ -213,17 +223,19 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
suffixIcon: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_searchQuery.isNotEmpty)
|
||||
if (viewState.channelsSearchText.isNotEmpty)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_searchDebounce?.cancel();
|
||||
_searchDebounce = null;
|
||||
_searchController.clear();
|
||||
setState(() {
|
||||
_searchQuery = '';
|
||||
});
|
||||
context
|
||||
.read<UiViewStateService>()
|
||||
.setChannelsSearchText('');
|
||||
},
|
||||
),
|
||||
_buildFilterButton(),
|
||||
_buildFilterButton(viewState),
|
||||
],
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
@@ -240,9 +252,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
const Duration(milliseconds: 300),
|
||||
() {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_searchQuery = value.toLowerCase();
|
||||
});
|
||||
context
|
||||
.read<UiViewStateService>()
|
||||
.setChannelsSearchText(value);
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -277,8 +289,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
),
|
||||
],
|
||||
)
|
||||
: (_sortOption == ChannelSortOption.manual &&
|
||||
_searchQuery.isEmpty)
|
||||
: (viewState.channelsSortOption ==
|
||||
ChannelSortOption.manual &&
|
||||
viewState.channelsSearchText.isEmpty)
|
||||
? ReorderableListView.builder(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
@@ -408,78 +421,96 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
return Card(
|
||||
key: ValueKey('channel_${channel.index}'),
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
minVerticalPadding: 0,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
visualDensity: const VisualDensity(vertical: -2),
|
||||
leading: Stack(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor: bgColor,
|
||||
child: Icon(icon, color: iconColor),
|
||||
),
|
||||
if (isCommunityChannel)
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
width: 14,
|
||||
height: 14,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.purple,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).cardColor,
|
||||
width: 2,
|
||||
child: GestureDetector(
|
||||
onSecondaryTapUp: PlatformInfo.isDesktop
|
||||
? (_) => _showChannelActions(
|
||||
context,
|
||||
connector,
|
||||
channelMessageStore,
|
||||
channel,
|
||||
)
|
||||
: null,
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
minVerticalPadding: 0,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
visualDensity: const VisualDensity(vertical: -2),
|
||||
leading: Stack(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor: bgColor,
|
||||
child: Icon(icon, color: iconColor),
|
||||
),
|
||||
if (isCommunityChannel)
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
width: 14,
|
||||
height: 14,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.purple,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).cardColor,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.people,
|
||||
size: 8,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
child: const Icon(Icons.people, size: 8, color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: Text(
|
||||
channel.name.isEmpty
|
||||
? context.l10n.channels_channelIndex(channel.index)
|
||||
: channel.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
subtitle: Text(subtitle, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (unreadCount > 0) ...[
|
||||
UnreadBadge(count: unreadCount),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
if (showDragHandle && dragIndex != null)
|
||||
ReorderableDelayedDragStartListener(
|
||||
index: dragIndex,
|
||||
child: Icon(
|
||||
Icons.drag_handle,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
title: Text(
|
||||
channel.name.isEmpty
|
||||
? context.l10n.channels_channelIndex(channel.index)
|
||||
: channel.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
subtitle: Text(
|
||||
subtitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (unreadCount > 0) ...[
|
||||
UnreadBadge(count: unreadCount),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
if (showDragHandle && dragIndex != null)
|
||||
ReorderableDelayedDragStartListener(
|
||||
index: dragIndex,
|
||||
child: Icon(
|
||||
Icons.drag_handle,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
connector.markChannelRead(channel.index);
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
if (context.mounted) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ChannelChatScreen(channel: channel),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
onLongPress: () => _showChannelActions(
|
||||
context,
|
||||
connector,
|
||||
channelMessageStore,
|
||||
channel,
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
connector.markChannelRead(channel.index);
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
if (context.mounted) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ChannelChatScreen(channel: channel),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
onLongPress: () => _showChannelActions(
|
||||
context,
|
||||
connector,
|
||||
channelMessageStore,
|
||||
channel,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -580,59 +611,40 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
await showDisconnectDialog(context, connector);
|
||||
}
|
||||
|
||||
Widget _buildFilterButton() {
|
||||
const actionSortManual = 0;
|
||||
const actionSortName = 1;
|
||||
const actionSortLatest = 2;
|
||||
const actionSortUnread = 3;
|
||||
|
||||
return SortFilterMenu(
|
||||
Widget _buildFilterButton(UiViewStateService viewState) {
|
||||
return SortFilterMenu<ChannelSortOption>(
|
||||
tooltip: context.l10n.listFilter_tooltip,
|
||||
sections: [
|
||||
SortFilterMenuSection(
|
||||
SortFilterMenuSection<ChannelSortOption>(
|
||||
title: context.l10n.channels_sortBy,
|
||||
options: [
|
||||
SortFilterMenuOption(
|
||||
value: actionSortManual,
|
||||
SortFilterMenuOption<ChannelSortOption>(
|
||||
value: ChannelSortOption.manual,
|
||||
label: context.l10n.channels_sortManual,
|
||||
checked: _sortOption == ChannelSortOption.manual,
|
||||
checked: viewState.channelsSortOption == ChannelSortOption.manual,
|
||||
),
|
||||
SortFilterMenuOption(
|
||||
value: actionSortName,
|
||||
SortFilterMenuOption<ChannelSortOption>(
|
||||
value: ChannelSortOption.name,
|
||||
label: context.l10n.channels_sortAZ,
|
||||
checked: _sortOption == ChannelSortOption.name,
|
||||
checked: viewState.channelsSortOption == ChannelSortOption.name,
|
||||
),
|
||||
SortFilterMenuOption(
|
||||
value: actionSortLatest,
|
||||
SortFilterMenuOption<ChannelSortOption>(
|
||||
value: ChannelSortOption.latestMessages,
|
||||
label: context.l10n.channels_sortLatestMessages,
|
||||
checked: _sortOption == ChannelSortOption.latestMessages,
|
||||
checked:
|
||||
viewState.channelsSortOption ==
|
||||
ChannelSortOption.latestMessages,
|
||||
),
|
||||
SortFilterMenuOption(
|
||||
value: actionSortUnread,
|
||||
SortFilterMenuOption<ChannelSortOption>(
|
||||
value: ChannelSortOption.unread,
|
||||
label: context.l10n.channels_sortUnread,
|
||||
checked: _sortOption == ChannelSortOption.unread,
|
||||
checked: viewState.channelsSortOption == ChannelSortOption.unread,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
onSelected: (action) {
|
||||
setState(() {
|
||||
switch (action) {
|
||||
case actionSortManual:
|
||||
_sortOption = ChannelSortOption.manual;
|
||||
break;
|
||||
case actionSortLatest:
|
||||
_sortOption = ChannelSortOption.latestMessages;
|
||||
break;
|
||||
case actionSortUnread:
|
||||
_sortOption = ChannelSortOption.unread;
|
||||
break;
|
||||
case actionSortName:
|
||||
default:
|
||||
_sortOption = ChannelSortOption.name;
|
||||
break;
|
||||
}
|
||||
});
|
||||
onSelected: (sortOption) {
|
||||
viewState.setChannelsSortOption(sortOption);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -640,11 +652,14 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
List<Channel> _filterAndSortChannels(
|
||||
List<Channel> channels,
|
||||
MeshCoreConnector connector,
|
||||
UiViewStateService viewState,
|
||||
) {
|
||||
var filtered = channels.where((channel) {
|
||||
if (_searchQuery.isEmpty) return true;
|
||||
if (viewState.channelsSearchText.isEmpty) return true;
|
||||
final label = _normalizeChannelName(channel);
|
||||
return label.toLowerCase().contains(_searchQuery);
|
||||
return label.toLowerCase().contains(
|
||||
viewState.channelsSearchText.toLowerCase(),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
int compareByName(Channel a, Channel b) {
|
||||
@@ -653,7 +668,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
return nameA.toLowerCase().compareTo(nameB.toLowerCase());
|
||||
}
|
||||
|
||||
switch (_sortOption) {
|
||||
switch (viewState.channelsSortOption) {
|
||||
case ChannelSortOption.manual:
|
||||
break;
|
||||
case ChannelSortOption.latestMessages:
|
||||
@@ -714,6 +729,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
bool isRegularHashtag = true;
|
||||
Community? selectedCommunity;
|
||||
|
||||
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
@@ -765,7 +782,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
);
|
||||
}
|
||||
|
||||
Widget? buildExpandedContent() {
|
||||
Widget? buildExpandedContent(
|
||||
ChannelMessageStore channelMessageStore,
|
||||
) {
|
||||
switch (selectedOption) {
|
||||
case 0: // Create Private Channel
|
||||
return Column(
|
||||
@@ -790,18 +809,15 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
children: [
|
||||
Expanded(
|
||||
child: FilledButton(
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
final name = nameController.text.trim();
|
||||
if (name.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
dialogContext,
|
||||
).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
dialogContext
|
||||
.l10n
|
||||
.channels_enterChannelName,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
dialogContext
|
||||
.l10n
|
||||
.channels_enterChannelName,
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -812,15 +828,19 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
psk[i] = random.nextInt(256);
|
||||
}
|
||||
Navigator.pop(dialogContext);
|
||||
connector.setChannel(nextIndex, name, psk);
|
||||
await connector.setChannel(
|
||||
nextIndex,
|
||||
name,
|
||||
psk,
|
||||
);
|
||||
await channelMessageStore.clearChannelMessages(
|
||||
nextIndex,
|
||||
);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.channels_channelAdded(
|
||||
name,
|
||||
),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.channels_channelAdded(name),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -874,15 +894,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
final name = nameController.text.trim();
|
||||
final pskHex = pskController.text.trim();
|
||||
if (name.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
dialogContext,
|
||||
).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
dialogContext
|
||||
.l10n
|
||||
.channels_enterChannelName,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
dialogContext
|
||||
.l10n
|
||||
.channels_enterChannelName,
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -891,15 +908,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
try {
|
||||
psk = Channel.parsePskHex(pskHex);
|
||||
} on FormatException {
|
||||
ScaffoldMessenger.of(
|
||||
dialogContext,
|
||||
).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
dialogContext
|
||||
.l10n
|
||||
.channels_pskMustBe32Hex,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
dialogContext
|
||||
.l10n
|
||||
.channels_pskMustBe32Hex,
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -907,13 +921,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
Navigator.pop(dialogContext);
|
||||
connector.setChannel(nextIndex, name, psk);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.channels_channelAdded(
|
||||
name,
|
||||
),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.channels_channelAdded(name),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -944,11 +955,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
Navigator.pop(dialogContext);
|
||||
connector.setChannel(nextIndex, 'Public', psk);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.channels_publicChannelAdded,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.channels_publicChannelAdded,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1074,15 +1084,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
onPressed: () async {
|
||||
var hashtag = hashtagController.text.trim();
|
||||
if (hashtag.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
dialogContext,
|
||||
).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
dialogContext
|
||||
.l10n
|
||||
.channels_enterChannelName,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
dialogContext
|
||||
.l10n
|
||||
.channels_enterChannelName,
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -1102,15 +1109,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
} else {
|
||||
// Community hashtag - HMAC derivation from community secret
|
||||
if (selectedCommunity == null) {
|
||||
ScaffoldMessenger.of(
|
||||
showDismissibleSnackBar(
|
||||
dialogContext,
|
||||
).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
dialogContext
|
||||
.l10n
|
||||
.community_selectCommunity,
|
||||
),
|
||||
content: Text(
|
||||
dialogContext
|
||||
.l10n
|
||||
.community_selectCommunity,
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -1136,12 +1140,11 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
psk,
|
||||
);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.channels_channelAdded(
|
||||
channelName,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.channels_channelAdded(
|
||||
channelName,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1236,13 +1239,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
onPressed: () async {
|
||||
final name = nameController.text.trim();
|
||||
if (name.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
dialogContext,
|
||||
).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
dialogContext.l10n.community_enterName,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
dialogContext.l10n.community_enterName,
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -1278,11 +1278,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
_loadCommunities();
|
||||
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.community_created(name),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.community_created(name),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1331,7 +1330,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
subtitle:
|
||||
dialogContext.l10n.channels_createPrivateChannelDesc,
|
||||
),
|
||||
if (selectedOption == 0) buildExpandedContent()!,
|
||||
if (selectedOption == 0)
|
||||
buildExpandedContent(_channelMessageStore)!,
|
||||
const Divider(height: 1),
|
||||
buildOptionTile(
|
||||
optionIndex: 1,
|
||||
@@ -1340,7 +1340,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
subtitle:
|
||||
dialogContext.l10n.channels_joinPrivateChannelDesc,
|
||||
),
|
||||
if (selectedOption == 1) buildExpandedContent()!,
|
||||
if (selectedOption == 1)
|
||||
buildExpandedContent(_channelMessageStore)!,
|
||||
if (!hasPublicChannel) ...[
|
||||
const Divider(height: 1),
|
||||
buildOptionTile(
|
||||
@@ -1350,7 +1351,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
subtitle:
|
||||
dialogContext.l10n.channels_joinPublicChannelDesc,
|
||||
),
|
||||
if (selectedOption == 2) buildExpandedContent()!,
|
||||
if (selectedOption == 2)
|
||||
buildExpandedContent(_channelMessageStore)!,
|
||||
],
|
||||
const Divider(height: 1),
|
||||
buildOptionTile(
|
||||
@@ -1360,7 +1362,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
subtitle:
|
||||
dialogContext.l10n.channels_joinHashtagChannelDesc,
|
||||
),
|
||||
if (selectedOption == 3) buildExpandedContent()!,
|
||||
if (selectedOption == 3)
|
||||
buildExpandedContent(_channelMessageStore)!,
|
||||
const Divider(height: 1),
|
||||
buildOptionTile(
|
||||
optionIndex: 4,
|
||||
@@ -1368,7 +1371,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
title: dialogContext.l10n.community_scanQr,
|
||||
subtitle: dialogContext.l10n.community_join,
|
||||
),
|
||||
if (selectedOption == 4) buildExpandedContent()!,
|
||||
if (selectedOption == 4)
|
||||
buildExpandedContent(_channelMessageStore)!,
|
||||
const Divider(height: 1),
|
||||
buildOptionTile(
|
||||
optionIndex: 5,
|
||||
@@ -1376,7 +1380,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
title: dialogContext.l10n.community_create,
|
||||
subtitle: dialogContext.l10n.community_createDesc,
|
||||
),
|
||||
if (selectedOption == 5) buildExpandedContent()!,
|
||||
if (selectedOption == 5)
|
||||
buildExpandedContent(_channelMessageStore)!,
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1465,10 +1470,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
try {
|
||||
psk = Channel.parsePskHex(pskHex);
|
||||
} on FormatException {
|
||||
ScaffoldMessenger.of(dialogContext).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(dialogContext.l10n.channels_pskMustBe32Hex),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
dialogContext,
|
||||
content: Text(dialogContext.l10n.channels_pskMustBe32Hex),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -1481,16 +1485,16 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
smazEnabled,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.channels_channelUpdated(name)),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.channels_channelUpdated(name)),
|
||||
);
|
||||
} catch (e, st) {
|
||||
debugPrint(st.toString());
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to update channel: $e')),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text('Failed to update channel: $e'),
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -1526,25 +1530,23 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
try {
|
||||
await connector.deleteChannel(channel.index);
|
||||
|
||||
channelMessageStore.clearChannelMessages(channel.index);
|
||||
await channelMessageStore.clearChannelMessages(channel.index);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.channels_channelDeleted(channel.name),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.channels_channelDeleted(channel.name),
|
||||
),
|
||||
);
|
||||
} catch (e, st) {
|
||||
if (!context.mounted) return;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.channels_channelDeleteFailed(channel.name),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.channels_channelDeleteFailed(channel.name),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1565,8 +1567,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
void _addPublicChannel(BuildContext context, MeshCoreConnector connector) {
|
||||
final psk = Channel.parsePskHex(Channel.publicChannelPsk);
|
||||
connector.setChannel(0, 'Public', psk);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.channels_publicChannelAdded)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.channels_publicChannelAdded),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1751,6 +1754,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
}
|
||||
|
||||
final channelCount = communityChannels.length;
|
||||
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -1780,12 +1784,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
_loadCommunities();
|
||||
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.community_deleted(community.name),
|
||||
),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.community_deleted(community.name)),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
+584
-193
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,89 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
|
||||
class ChromeRequiredScreen extends StatelessWidget {
|
||||
const ChromeRequiredScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final theme = Theme.of(context);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: isDark
|
||||
? [const Color(0xFF1A1A1A), const Color(0xFF0D0D0D)]
|
||||
: [const Color(0xFFF5F7FA), const Color(0xFFE4E7EB)],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.browser_not_supported_rounded,
|
||||
size: 80,
|
||||
color: Colors.orange,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
l10n.scanner_chromeRequired,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.white : Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.scanner_chromeRequiredMessage,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: isDark ? Colors.white70 : Colors.black54,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
// We can't really "fix" it for them other than telling them to use Chrome
|
||||
// but we can provide a nice visual.
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
border: Border.all(color: Colors.blue.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.info_outline, size: 20, color: Colors.blue),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
"Web Bluetooth requires a Chromium browser",
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.blue,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import '../models/community.dart';
|
||||
import '../storage/community_store.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../widgets/qr_scanner_widget.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
/// Screen for scanning community QR codes to join communities.
|
||||
///
|
||||
@@ -51,6 +52,9 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
|
||||
_isProcessing = true;
|
||||
});
|
||||
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||
|
||||
try {
|
||||
// Parse the community data
|
||||
final community = Community.fromQrData(const Uuid().v4(), data);
|
||||
@@ -73,11 +77,10 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.community_invalidQrCode),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.community_invalidQrCode),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
@@ -90,12 +93,11 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
|
||||
}
|
||||
|
||||
void _showInvalidQrError(BuildContext context) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.community_invalidQrCode),
|
||||
backgroundColor: Colors.orange,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.community_invalidQrCode),
|
||||
backgroundColor: Colors.orange,
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -209,6 +211,8 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
|
||||
bool addPublicChannel,
|
||||
) async {
|
||||
// Save community to local storage
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||
await _communityStore.addCommunity(community);
|
||||
|
||||
// Optionally add the community public channel to the device
|
||||
@@ -224,11 +228,10 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.community_joined(community.name)),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.community_joined(community.name)),
|
||||
backgroundColor: Colors.green,
|
||||
);
|
||||
|
||||
// Return to previous screen
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
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.setPollingInterval(1);
|
||||
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();
|
||||
_connector?.setPollingInterval(30);
|
||||
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;
|
||||
}
|
||||
}
|
||||
+646
-437
File diff suppressed because it is too large
Load Diff
@@ -1,282 +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, connector),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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, MeshCoreConnector connector) {
|
||||
return QuickSwitchBar(
|
||||
selectedIndex: _quickIndex,
|
||||
onDestinationSelected: (index) {
|
||||
_openQuickDestination(index, context);
|
||||
},
|
||||
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
|
||||
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../utils/contact_search.dart';
|
||||
import '../utils/platform_info.dart';
|
||||
import '../widgets/app_bar.dart';
|
||||
import '../widgets/list_filter_widget.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
enum DiscoverySortOption { lastSeen, name, type }
|
||||
|
||||
class DiscoveryScreen extends StatefulWidget {
|
||||
const DiscoveryScreen({super.key});
|
||||
|
||||
@override
|
||||
State<DiscoveryScreen> createState() => _DiscoveryScreenState();
|
||||
}
|
||||
|
||||
class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
String searchQuery = '';
|
||||
ContactSortOption sortOption = ContactSortOption.lastSeen;
|
||||
bool showUnreadOnly = false;
|
||||
ContactTypeFilter typeFilter = ContactTypeFilter.all;
|
||||
DiscoverySortOption discoverySortOption = DiscoverySortOption.lastSeen;
|
||||
Timer? _searchDebounce;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
_searchDebounce?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
DateTime _resolveLastSeen(Contact contact) {
|
||||
if (contact.type != advTypeChat) return contact.lastSeen;
|
||||
return contact.lastMessageAt.isAfter(contact.lastSeen)
|
||||
? contact.lastMessageAt
|
||||
: contact.lastSeen;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
|
||||
final discoveredContacts = connector.discoveredContacts;
|
||||
final filteredAndSorted = _filterAndSortContacts(
|
||||
discoveredContacts,
|
||||
connector,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: AppBarTitle(
|
||||
l10n.discoveredContacts_Title,
|
||||
indicators: false,
|
||||
subtitle: false,
|
||||
),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.delete, color: Colors.red),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.l10n.discoveredContacts_deleteContactAll),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
_deleteContacts(context, connector);
|
||||
},
|
||||
),
|
||||
],
|
||||
icon: const Icon(Icons.more_vert),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
_buildFilters(filteredAndSorted, connector),
|
||||
Expanded(
|
||||
child: discoveredContacts.isEmpty
|
||||
? Center(child: Text(l10n.contacts_noContacts))
|
||||
: filteredAndSorted.isEmpty
|
||||
? Center(child: Text(l10n.discoveredContacts_noMatching))
|
||||
: ListView.builder(
|
||||
itemCount: filteredAndSorted.length,
|
||||
itemBuilder: (context, index) {
|
||||
final contact = filteredAndSorted[index];
|
||||
final tile = ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: _getTypeColor(contact.type),
|
||||
child: Icon(
|
||||
_getTypeIcon(contact.type),
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
contact.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
contact.shortPubKeyHex,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
// Clamp text scaling in trailing section to prevent overflow while
|
||||
// maintaining accessibility. Primary content (title/subtitle) scales normally.
|
||||
trailing: MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
textScaler: TextScaler.linear(
|
||||
MediaQuery.textScalerOf(
|
||||
context,
|
||||
).scale(1.0).clamp(1.0, 1.3),
|
||||
),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: 120,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
_formatLastSeen(
|
||||
context,
|
||||
_resolveLastSeen(contact),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (contact.hasLocation)
|
||||
Icon(
|
||||
Icons.location_on,
|
||||
size: 14,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
if (contact.rawPacket != null)
|
||||
const SizedBox(width: 2),
|
||||
if (contact.rawPacket != null)
|
||||
Icon(
|
||||
Icons.cell_tower,
|
||||
size: 14,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
connector.importDiscoveredContact(contact);
|
||||
},
|
||||
onLongPress: () =>
|
||||
_showContactContextMenu(contact, connector),
|
||||
);
|
||||
if (PlatformInfo.isDesktop) {
|
||||
return GestureDetector(
|
||||
onSecondaryTapUp: (_) =>
|
||||
_showContactContextMenu(contact, connector),
|
||||
child: tile,
|
||||
);
|
||||
}
|
||||
return tile;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showContactContextMenu(
|
||||
Contact contact,
|
||||
MeshCoreConnector connector,
|
||||
) async {
|
||||
final action = await showModalBottomSheet<String>(
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
builder: (sheetContext) {
|
||||
final l10n = context.l10n;
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.add_reaction_sharp),
|
||||
title: Text(l10n.discoveredContacts_addContact),
|
||||
onTap: () => Navigator.of(sheetContext).pop('import_contact'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.copy),
|
||||
title: Text(l10n.discoveredContacts_copyContact),
|
||||
onTap: () => Navigator.of(sheetContext).pop('copy_contact'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete),
|
||||
title: Text(l10n.discoveredContacts_deleteContact),
|
||||
onTap: () => Navigator.of(sheetContext).pop('delete_contact'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (!mounted || action == null) return;
|
||||
|
||||
switch (action) {
|
||||
case 'import_contact':
|
||||
connector.importDiscoveredContact(contact);
|
||||
break;
|
||||
case 'copy_contact':
|
||||
if (contact.rawPacket == null) return;
|
||||
final hexString = pubKeyToHex(contact.rawPacket!);
|
||||
Clipboard.setData(ClipboardData(text: "meshcore://$hexString"));
|
||||
if (!mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.contacts_contactAdvertCopied),
|
||||
);
|
||||
break;
|
||||
case 'delete_contact':
|
||||
connector.removeDiscoveredContact(contact);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _deleteContacts(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(l10n.common_deleteAll),
|
||||
content: Text(l10n.discoveredContacts_deleteContactAllContent),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
connector.removeAllDiscoveredContacts();
|
||||
},
|
||||
child: Text(l10n.common_deleteAll),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilters(
|
||||
List<Contact> filteredAndSorted,
|
||||
MeshCoreConnector connector,
|
||||
) {
|
||||
String hintText = "";
|
||||
switch (typeFilter) {
|
||||
case ContactTypeFilter.all:
|
||||
hintText = context.l10n.contacts_searchContacts(
|
||||
filteredAndSorted.length,
|
||||
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||
);
|
||||
break;
|
||||
case ContactTypeFilter.users:
|
||||
hintText = context.l10n.contacts_searchUsers(
|
||||
filteredAndSorted.length,
|
||||
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||
);
|
||||
break;
|
||||
case ContactTypeFilter.repeaters:
|
||||
hintText = context.l10n.contacts_searchRepeaters(
|
||||
filteredAndSorted.length,
|
||||
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||
);
|
||||
break;
|
||||
case ContactTypeFilter.rooms:
|
||||
hintText = context.l10n.contacts_searchRoomServers(
|
||||
filteredAndSorted.length,
|
||||
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||
);
|
||||
break;
|
||||
case ContactTypeFilter.favorites:
|
||||
hintText = context.l10n.contacts_searchFavorites(
|
||||
filteredAndSorted.length,
|
||||
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (searchQuery.isNotEmpty)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
setState(() {
|
||||
searchQuery = '';
|
||||
});
|
||||
},
|
||||
),
|
||||
_buildFilterButton(context, connector),
|
||||
],
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
_searchDebounce?.cancel();
|
||||
_searchDebounce = Timer(const Duration(milliseconds: 300), () {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
searchQuery = value.toLowerCase();
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterButton(BuildContext context, MeshCoreConnector connector) {
|
||||
return DiscoveryContactsFilterMenu(
|
||||
sortOption: sortOption,
|
||||
typeFilter: typeFilter,
|
||||
onSortChanged: (value) {
|
||||
setState(() {
|
||||
sortOption = value;
|
||||
});
|
||||
},
|
||||
onTypeFilterChanged: (value) {
|
||||
setState(() {
|
||||
typeFilter = value;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<Contact> _filterAndSortContacts(
|
||||
List<Contact> contacts,
|
||||
MeshCoreConnector connector,
|
||||
) {
|
||||
var filtered = contacts.where((contact) {
|
||||
if (searchQuery.isEmpty) return true;
|
||||
return matchesDiscoveryContactQuery(contact, searchQuery);
|
||||
}).toList();
|
||||
|
||||
filtered = filtered.where((contact) {
|
||||
return !connector.knownContactKeys.contains(contact.publicKeyHex);
|
||||
}).toList();
|
||||
|
||||
// Filter out own node from the list
|
||||
if (connector.selfPublicKey != null) {
|
||||
final selfPubKeyHex = pubKeyToHex(connector.selfPublicKey!);
|
||||
filtered = filtered.where((contact) {
|
||||
return contact.publicKeyHex != selfPubKeyHex;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
if (typeFilter != ContactTypeFilter.all) {
|
||||
filtered = filtered.where(_matchesTypeFilter).toList();
|
||||
}
|
||||
|
||||
switch (sortOption) {
|
||||
case ContactSortOption.lastSeen:
|
||||
filtered.sort((a, b) => b.lastSeen.compareTo(a.lastSeen));
|
||||
break;
|
||||
case ContactSortOption.name:
|
||||
filtered.sort(
|
||||
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
bool _matchesTypeFilter(Contact contact) {
|
||||
switch (typeFilter) {
|
||||
case ContactTypeFilter.all:
|
||||
return true;
|
||||
case ContactTypeFilter.users:
|
||||
return contact.type == advTypeChat;
|
||||
case ContactTypeFilter.repeaters:
|
||||
return contact.type == advTypeRepeater;
|
||||
case ContactTypeFilter.rooms:
|
||||
return contact.type == advTypeRoom;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getTypeIcon(int type) {
|
||||
switch (type) {
|
||||
case advTypeChat:
|
||||
return Icons.chat;
|
||||
case advTypeRepeater:
|
||||
return Icons.cell_tower;
|
||||
case advTypeRoom:
|
||||
return Icons.group;
|
||||
case advTypeSensor:
|
||||
return Icons.sensors;
|
||||
default:
|
||||
return Icons.device_unknown;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getTypeColor(int type) {
|
||||
switch (type) {
|
||||
case advTypeChat:
|
||||
return Colors.blue;
|
||||
case advTypeRepeater:
|
||||
return Colors.orange;
|
||||
case advTypeRoom:
|
||||
return Colors.purple;
|
||||
case advTypeSensor:
|
||||
return Colors.green;
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
String _formatLastSeen(BuildContext context, DateTime lastSeen) {
|
||||
final now = DateTime.now();
|
||||
final diff = now.difference(lastSeen);
|
||||
|
||||
if (diff.isNegative || diff.inMinutes < 5) {
|
||||
return context.l10n.contacts_lastSeenNow;
|
||||
}
|
||||
if (diff.inMinutes < 60) {
|
||||
return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes);
|
||||
}
|
||||
if (diff.inHours < 24) {
|
||||
final hours = diff.inHours;
|
||||
return hours == 1
|
||||
? context.l10n.contacts_lastSeenHourAgo
|
||||
: context.l10n.contacts_lastSeenHoursAgo(hours);
|
||||
}
|
||||
final days = diff.inDays;
|
||||
return days == 1
|
||||
? context.l10n.contacts_lastSeenDayAgo
|
||||
: context.l10n.contacts_lastSeenDaysAgo(days);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import '../l10n/l10n.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/map_tile_cache_service.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
class MapCacheScreen extends StatefulWidget {
|
||||
const MapCacheScreen({super.key});
|
||||
@@ -112,15 +113,17 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
Future<void> _startDownload() async {
|
||||
final bounds = _selectedBounds;
|
||||
if (bounds == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.mapCache_selectAreaFirst)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.mapCache_selectAreaFirst),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_estimatedTiles == 0) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.mapCache_noTilesToDownload)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.mapCache_noTilesToDownload),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -182,9 +185,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
result.failed,
|
||||
)
|
||||
: context.l10n.mapCache_cachedTiles(result.downloaded);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(message)));
|
||||
showDismissibleSnackBar(context, content: Text(message));
|
||||
}
|
||||
|
||||
Future<void> _clearCache() async {
|
||||
@@ -210,8 +211,9 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
final cacheService = context.read<MapTileCacheService>();
|
||||
await cacheService.clearCache();
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.mapCache_offlineCacheCleared)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.mapCache_offlineCacheCleared),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+658
-92
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ import '../connector/meshcore_protocol.dart';
|
||||
import '../services/repeater_command_service.dart';
|
||||
import '../widgets/path_management_dialog.dart';
|
||||
import '../widgets/snr_indicator.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
class NeighborsScreen extends StatefulWidget {
|
||||
final Contact repeater;
|
||||
@@ -44,6 +45,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();
|
||||
@@ -124,12 +143,11 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||
|
||||
void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) {
|
||||
final buffer = BufferReader(frame);
|
||||
final contacts = connector.allContactsUnfiltered;
|
||||
try {
|
||||
final neighborCount = buffer.readUInt16LE();
|
||||
final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE());
|
||||
connector.contacts.where((c) => c.type == advTypeRepeater).forEach((
|
||||
repeater,
|
||||
) {
|
||||
contacts.where((c) => c.type == advTypeRepeater).forEach((repeater) {
|
||||
for (var neighborData in parsedNeighbors) {
|
||||
final publicKey = neighborData['publicKey'];
|
||||
if (listEquals(
|
||||
@@ -146,11 +164,10 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||
_neighborCount = neighborCount;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.neighbors_receivedData),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.neighbors_receivedData),
|
||||
backgroundColor: Colors.green,
|
||||
);
|
||||
_statusTimeout?.cancel();
|
||||
if (!mounted) return;
|
||||
@@ -164,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;
|
||||
|
||||
@@ -214,11 +224,10 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||
_isLoading = false;
|
||||
_isLoaded = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.neighbors_requestTimedOut),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.neighbors_requestTimedOut),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
_recordStatusResult(false);
|
||||
});
|
||||
@@ -229,11 +238,10 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||
_isLoaded = false;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.neighbors_errorLoading(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.neighbors_errorLoading(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+278
-44
@@ -52,16 +52,22 @@ class PathTraceMapScreen extends StatefulWidget {
|
||||
final String title;
|
||||
final Uint8List path;
|
||||
final int? repeaterId;
|
||||
final bool flipPathRound;
|
||||
final bool reversePathRound;
|
||||
final bool flipPathAround;
|
||||
final bool reversePathAround;
|
||||
final Contact? targetContact;
|
||||
final int pathHashByteWidth;
|
||||
final List<Contact>? pathContacts;
|
||||
|
||||
const PathTraceMapScreen({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.path,
|
||||
this.repeaterId,
|
||||
this.flipPathRound = false,
|
||||
this.reversePathRound = false,
|
||||
this.flipPathAround = false,
|
||||
this.reversePathAround = false,
|
||||
this.targetContact,
|
||||
this.pathHashByteWidth = pathHashSize,
|
||||
this.pathContacts,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -70,6 +76,8 @@ class PathTraceMapScreen extends StatefulWidget {
|
||||
|
||||
class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
static const double _labelZoomThreshold = 8.5;
|
||||
//miles to meters conversion for filtering out repeaters that are too far from the last known GPS hop to be a likely match, to avoid false matches that throw off the inferred positions of other hops in the path
|
||||
static const double _maxRepeaterMatchDistanceMeters = 40 * 1609.344;
|
||||
|
||||
StreamSubscription<Uint8List>? _frameSubscription;
|
||||
Timer? _timeoutTimer;
|
||||
@@ -78,6 +86,11 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
bool _failed2Loaded = false;
|
||||
bool _hasData = false;
|
||||
PathTraceData? _traceData;
|
||||
// Inferred positions for hops that have no GPS location, keyed by hop byte.
|
||||
Map<int, LatLng> _inferredHopPositions = {};
|
||||
// Endpoint position for the target contact (GPS or guessed).
|
||||
LatLng? _targetContactPosition;
|
||||
bool _targetContactIsGuessed = false;
|
||||
List<LatLng> _points = <LatLng>[];
|
||||
List<Polyline> _polylines = [];
|
||||
LatLng? _initialCenter = LatLng(0, 0);
|
||||
@@ -86,6 +99,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
ValueKey<String> _mapKey = const ValueKey('initial');
|
||||
double _pathDistanceMeters = 0.0;
|
||||
bool _showNodeLabels = true;
|
||||
Contact? _targetContact;
|
||||
|
||||
String _formatPathPrefixes(Uint8List pathBytes) {
|
||||
return pathBytes
|
||||
@@ -107,14 +121,42 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Uint8List addReturnPath(Uint8List pathBytes) {
|
||||
Uint8List? traceBytes;
|
||||
final len = (pathBytes.length + pathBytes.length - 1);
|
||||
traceBytes = Uint8List(len);
|
||||
for (int i = 0; i < pathBytes.length; i++) {
|
||||
traceBytes[i] = pathBytes[i];
|
||||
if (i < pathBytes.length - 1) {
|
||||
traceBytes[len - 1 - i] = pathBytes[i];
|
||||
Uint8List buildPath(Uint8List pathBytes) {
|
||||
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] = pk?[0] ?? 0;
|
||||
return traceBytes;
|
||||
}
|
||||
|
||||
if (widget.targetContact?.type == advTypeRepeater ||
|
||||
widget.targetContact?.type == advTypeRoom) {
|
||||
final len = (pathBytes.length + pathBytes.length + 1);
|
||||
traceBytes = Uint8List(len);
|
||||
traceBytes[pathBytes.length] = widget.targetContact?.publicKey[0] ?? 0;
|
||||
for (int i = 0; i < pathBytes.length; i++) {
|
||||
traceBytes[i] = pathBytes[i];
|
||||
if (i < pathBytes.length) {
|
||||
traceBytes[len - 1 - i] = pathBytes[i];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (pathBytes.length < 2) {
|
||||
return pathBytes[0] == 0 ? Uint8List(0) : pathBytes;
|
||||
}
|
||||
final len = (pathBytes.length + pathBytes.length - 1);
|
||||
traceBytes = Uint8List(len);
|
||||
for (int i = 0; i < pathBytes.length; i++) {
|
||||
traceBytes[i] = pathBytes[i];
|
||||
if (i < pathBytes.length - 1) {
|
||||
traceBytes[len - 1 - i] = pathBytes[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
return traceBytes;
|
||||
@@ -128,17 +170,17 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
final Uint8List path;
|
||||
|
||||
Uint8List pathTmp = widget.reversePathRound
|
||||
final pathTmp = widget.reversePathAround
|
||||
? Uint8List.fromList(widget.path.reversed.toList())
|
||||
: widget.path;
|
||||
|
||||
if (widget.flipPathRound) {
|
||||
path = addReturnPath(pathTmp);
|
||||
} else {
|
||||
path = pathTmp;
|
||||
}
|
||||
final path = widget.flipPathAround ? buildPath(pathTmp) : pathTmp;
|
||||
|
||||
appLogger.info(
|
||||
'Initiating path trace with path: ${_formatPathPrefixes(path)}',
|
||||
tag: 'PathTraceMapScreen',
|
||||
noNotify: !mounted,
|
||||
);
|
||||
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
final frame = buildTraceReq(
|
||||
@@ -228,37 +270,163 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
.toList();
|
||||
|
||||
Map<int, Contact> pathContacts = {};
|
||||
|
||||
connector.contacts.where((c) => c.type != advTypeChat).forEach((
|
||||
repeater,
|
||||
) {
|
||||
for (var repeaterData in pathData) {
|
||||
if (listEquals(
|
||||
repeater.publicKey.sublist(0, 1),
|
||||
Uint8List.fromList([repeaterData]),
|
||||
)) {
|
||||
pathContacts[repeaterData] = repeater;
|
||||
Contact lastContact = Contact(
|
||||
path: Uint8List(0),
|
||||
pathLength: 0,
|
||||
publicKey: connector.selfPublicKey ?? Uint8List(0),
|
||||
name: context.l10n.pathTrace_you,
|
||||
type: advTypeChat,
|
||||
latitude: connector.selfLatitude,
|
||||
longitude: connector.selfLongitude,
|
||||
lastSeen: DateTime.now(),
|
||||
);
|
||||
if (widget.pathContacts != null) {
|
||||
pathContacts = {for (var c in widget.pathContacts!) c.publicKey[0]: c};
|
||||
} else {
|
||||
final contacts = connector.allContactsUnfiltered;
|
||||
contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
|
||||
if (lastContact.latitude != null &&
|
||||
lastContact.longitude != null &&
|
||||
repeater.hasLocation &&
|
||||
lastContact.hasLocation &&
|
||||
Distance().distance(
|
||||
LatLng(lastContact.latitude!, lastContact.longitude!),
|
||||
LatLng(repeater.latitude!, repeater.longitude!),
|
||||
) >
|
||||
_maxRepeaterMatchDistanceMeters) {
|
||||
return; //skip reapeaters that are far away from the last one with known GPS, to avoid false matches
|
||||
}
|
||||
for (var repeaterData in pathData) {
|
||||
if (listEquals(
|
||||
repeater.publicKey.sublist(0, 1),
|
||||
Uint8List.fromList([repeaterData]),
|
||||
)) {
|
||||
pathContacts[repeaterData] = repeater;
|
||||
lastContact = repeater;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// For hops with no GPS contact, infer position from other contacts
|
||||
// with known GPS that share the same last-hop byte.
|
||||
final Map<int, LatLng> inferredPositions = {};
|
||||
for (final hop in pathData) {
|
||||
final contact = pathContacts[hop];
|
||||
if (contact != null && contact.hasLocation) continue;
|
||||
final peers = connector.contacts
|
||||
.where(
|
||||
(c) => c.hasLocation && c.path.isNotEmpty && c.path.last == hop,
|
||||
)
|
||||
.toList();
|
||||
if (peers.isNotEmpty) {
|
||||
final lat =
|
||||
peers.map((c) => c.latitude!).reduce((a, b) => a + b) /
|
||||
peers.length;
|
||||
final lon =
|
||||
peers.map((c) => c.longitude!).reduce((a, b) => a + b) /
|
||||
peers.length;
|
||||
inferredPositions[hop] = LatLng(lat, lon);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_hasData = true;
|
||||
_inferredHopPositions = inferredPositions;
|
||||
_traceData = PathTraceData(
|
||||
pathData: pathData,
|
||||
snrData: snrData,
|
||||
pathContacts: pathContacts,
|
||||
);
|
||||
// Compute endpoint position for the target contact.
|
||||
LatLng? targetPos;
|
||||
bool targetGuessed = false;
|
||||
_targetContact = widget.targetContact;
|
||||
|
||||
if (_targetContact != null) {
|
||||
final tc = _targetContact!;
|
||||
if (tc.hasLocation) {
|
||||
targetPos = LatLng(tc.latitude!, tc.longitude!);
|
||||
} else if (widget.path.length > 1) {
|
||||
// Infer from the last hop: average GPS contacts sharing that hop.
|
||||
// For a round-trip path (flipPathAround/reversePathAround), the target-side hop
|
||||
// sits in the middle of the symmetric sequence; .last is the local side.
|
||||
final lastHop = widget.reversePathAround
|
||||
? widget.path.first
|
||||
: widget.path.last;
|
||||
|
||||
final peers = connector.allContacts
|
||||
.where(
|
||||
(c) =>
|
||||
c.hasLocation &&
|
||||
c.path.isNotEmpty &&
|
||||
c.path.last == lastHop,
|
||||
)
|
||||
.toList();
|
||||
if (peers.isNotEmpty) {
|
||||
final lat =
|
||||
peers.map((c) => c.latitude!).reduce((a, b) => a + b) /
|
||||
peers.length;
|
||||
final lon =
|
||||
peers.map((c) => c.longitude!).reduce((a, b) => a + b) /
|
||||
peers.length;
|
||||
const offsetDeg = 0.003;
|
||||
final angle = (tc.publicKey[1] / 255.0) * 2 * pi;
|
||||
targetPos = LatLng(
|
||||
lat + offsetDeg * cos(angle),
|
||||
lon + offsetDeg * sin(angle),
|
||||
);
|
||||
targetGuessed = true;
|
||||
} else if (inferredPositions.containsKey(lastHop)) {
|
||||
final lat = inferredPositions[lastHop]!.latitude;
|
||||
final lon = inferredPositions[lastHop]!.longitude;
|
||||
const offsetDeg = 0.003;
|
||||
final angle = (tc.publicKey[1] / 255.0) * 2 * pi;
|
||||
targetPos = LatLng(
|
||||
lat + offsetDeg * cos(angle),
|
||||
lon + offsetDeg * sin(angle),
|
||||
);
|
||||
targetGuessed = true;
|
||||
} else {
|
||||
// As a last resort, just place it at the same position as the last hop.
|
||||
final contact = pathContacts[lastHop];
|
||||
if (contact != null && contact.hasLocation) {
|
||||
const offsetDeg = 0.003;
|
||||
final angle = (tc.publicKey[1] / 255.0) * 2 * pi;
|
||||
targetPos = LatLng(
|
||||
contact.latitude! + offsetDeg * cos(angle),
|
||||
contact.longitude! + offsetDeg * sin(angle),
|
||||
);
|
||||
targetGuessed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_targetContactPosition = targetPos;
|
||||
_targetContactIsGuessed = targetGuessed;
|
||||
|
||||
_points = <LatLng>[];
|
||||
_points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
||||
int hopLast = 0;
|
||||
int hopLastLast = 0;
|
||||
for (final hop in _traceData!.pathData) {
|
||||
if (hop == hopLastLast && widget.flipPathAround) {
|
||||
break; //skip duplicate hops in round-trip paths
|
||||
}
|
||||
final contact = _traceData!.pathContacts[hop];
|
||||
if (contact != null &&
|
||||
contact.hasLocation &&
|
||||
contact.latitude != null &&
|
||||
contact.longitude != null) {
|
||||
if (contact != null && contact.hasLocation) {
|
||||
_points.add(LatLng(contact.latitude!, contact.longitude!));
|
||||
} else {
|
||||
final inferred = inferredPositions[hop];
|
||||
if (inferred != null) _points.add(inferred);
|
||||
}
|
||||
hopLastLast = hopLast;
|
||||
hopLast = hop;
|
||||
}
|
||||
if (targetPos != null) {
|
||||
if (_targetContact != null && _targetContact!.type == advTypeChat) {
|
||||
_points.add(targetPos);
|
||||
}
|
||||
}
|
||||
_polylines = _points.length > 1
|
||||
@@ -349,7 +517,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_hasData) _buildMapPathTrace(context, tileCache),
|
||||
if (_hasData)
|
||||
_buildMapPathTrace(context, tileCache, _targetContact),
|
||||
if (_points.isEmpty &&
|
||||
!_hasData &&
|
||||
!_isLoading &&
|
||||
@@ -378,12 +547,28 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
List<Marker> _buildHopMarkers(
|
||||
List<int> pathData, {
|
||||
required bool showLabels,
|
||||
required Contact? target,
|
||||
}) {
|
||||
final markers = <Marker>[];
|
||||
int hopLast = 0;
|
||||
int hopLastLast = 0;
|
||||
for (final hop in pathData) {
|
||||
final contact = _traceData!.pathContacts[hop];
|
||||
if (contact == null || !contact.hasLocation) continue;
|
||||
final point = LatLng(contact.latitude!, contact.longitude!);
|
||||
final inferred = _inferredHopPositions[hop];
|
||||
final hasGps = contact != null && contact.hasLocation;
|
||||
if (hop == hopLastLast && widget.flipPathAround) {
|
||||
continue; //skip duplicate hops in round-trip paths
|
||||
}
|
||||
if (!hasGps && inferred == null) {
|
||||
hopLastLast = hopLast;
|
||||
hopLast = hop;
|
||||
continue; //skip hops with no GPS and no inferred position
|
||||
}
|
||||
final point = hasGps
|
||||
? LatLng(contact.latitude!, contact.longitude!)
|
||||
: inferred!;
|
||||
final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||
|
||||
markers.add(
|
||||
Marker(
|
||||
point: point,
|
||||
@@ -392,7 +577,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green,
|
||||
color: hasGps
|
||||
? Colors.green
|
||||
: Colors.orange.withValues(alpha: 0.75),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
@@ -405,10 +592,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
contact.publicKey
|
||||
.sublist(0, 1)
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||
.join(),
|
||||
hasGps ? label : '~$label',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -419,8 +603,15 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
),
|
||||
);
|
||||
if (showLabels) {
|
||||
markers.add(_buildNodeLabelMarker(point: point, label: contact.name));
|
||||
markers.add(
|
||||
_buildNodeLabelMarker(
|
||||
point: point,
|
||||
label: contact?.name ?? '~$label',
|
||||
),
|
||||
);
|
||||
}
|
||||
hopLastLast = hopLast;
|
||||
hopLast = hop;
|
||||
}
|
||||
|
||||
final selfLat = context.read<MeshCoreConnector>().selfLatitude;
|
||||
@@ -468,6 +659,47 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
// Add target contact endpoint marker.
|
||||
final targetPos = _targetContactPosition;
|
||||
if (targetPos != null && target != null && target.type == advTypeChat) {
|
||||
final isGuessed = _targetContactIsGuessed;
|
||||
final targetName = target.name;
|
||||
markers.add(
|
||||
Marker(
|
||||
point: targetPos,
|
||||
width: 35,
|
||||
height: 35,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: isGuessed
|
||||
? Colors.purple.withValues(alpha: 0.55)
|
||||
: Colors.red,
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const Icon(Icons.person, color: Colors.white, size: 18),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (showLabels) {
|
||||
markers.add(
|
||||
_buildNodeLabelMarker(
|
||||
point: targetPos,
|
||||
label: isGuessed ? '~$targetName' : targetName,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return markers;
|
||||
}
|
||||
|
||||
@@ -567,6 +799,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
Widget _buildMapPathTrace(
|
||||
BuildContext context,
|
||||
MapTileCacheService tileCache,
|
||||
Contact? target,
|
||||
) {
|
||||
return FlutterMap(
|
||||
key: _mapKey,
|
||||
@@ -605,6 +838,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
markers: _buildHopMarkers(
|
||||
_traceData!.pathData,
|
||||
showLabels: _showNodeLabels,
|
||||
target: target,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../connector/meshcore_protocol.dart';
|
||||
import '../widgets/debug_frame_viewer.dart';
|
||||
import '../services/repeater_command_service.dart';
|
||||
import '../widgets/path_management_dialog.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
class RepeaterCliScreen extends StatefulWidget {
|
||||
final Contact repeater;
|
||||
@@ -35,13 +36,15 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
||||
|
||||
// Common commands for quick access
|
||||
late final List<Map<String, String>> _quickCommands = [
|
||||
{'labelKey': 'advertise', 'command': 'advert'},
|
||||
{'labelKey': 'getName', 'command': 'get name'},
|
||||
{'labelKey': 'getRadio', 'command': 'get radio'},
|
||||
{'labelKey': 'getTx', 'command': 'get tx'},
|
||||
{'labelKey': 'discovery', 'command': 'discover.neighbors'},
|
||||
{'labelKey': 'neighbors', 'command': 'neighbors'},
|
||||
{'labelKey': 'version', 'command': 'ver'},
|
||||
{'labelKey': 'advertise', 'command': 'advert'},
|
||||
{'labelKey': 'clock', 'command': 'clock'},
|
||||
{'labelKey': 'clock sync', 'command': 'clock sync'},
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -77,11 +80,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) {
|
||||
@@ -323,8 +337,9 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
||||
if (_commandController.text.trim().isNotEmpty) {
|
||||
_sendCommand(showDebug: true);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.repeater_enterCommandFirst)),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.repeater_enterCommandFirst),
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -396,6 +411,10 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
||||
return l10n.repeater_cliQuickAdvertise;
|
||||
case 'clock':
|
||||
return l10n.repeater_cliQuickClock;
|
||||
case 'clock sync':
|
||||
return l10n.repeater_cliQuickClockSync;
|
||||
case 'discovery':
|
||||
return l10n.repeater_cliQuickDiscovery;
|
||||
default:
|
||||
return key;
|
||||
}
|
||||
|
||||
@@ -13,11 +13,13 @@ import 'neighbors_screen.dart';
|
||||
class RepeaterHubScreen extends StatelessWidget {
|
||||
final Contact repeater;
|
||||
final String password;
|
||||
final bool isAdmin;
|
||||
|
||||
const RepeaterHubScreen({
|
||||
super.key,
|
||||
required this.repeater,
|
||||
required this.password,
|
||||
required this.isAdmin,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -33,11 +35,18 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
repeater.type == advTypeRepeater
|
||||
? l10n.repeater_management
|
||||
: l10n.room_management,
|
||||
),
|
||||
if (isAdmin)
|
||||
Text(
|
||||
repeater.type == advTypeRepeater
|
||||
? l10n.repeater_management
|
||||
: l10n.room_management,
|
||||
),
|
||||
if (!isAdmin)
|
||||
Text(
|
||||
repeater.type == advTypeRepeater
|
||||
? l10n.repeater_guest
|
||||
: l10n.room_guest,
|
||||
),
|
||||
Text(
|
||||
repeater.name,
|
||||
style: const TextStyle(
|
||||
@@ -113,64 +122,67 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.battery_full),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.appSettings_batteryChemistry,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
if (isAdmin)
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.battery_full),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.appSettings_batteryChemistry,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: chemistry,
|
||||
isExpanded: true,
|
||||
decoration: const InputDecoration(
|
||||
border: UnderlineInputBorder(),
|
||||
isDense: true,
|
||||
],
|
||||
),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
settingsService.setBatteryChemistryForRepeater(
|
||||
repeater.publicKeyHex,
|
||||
value,
|
||||
);
|
||||
},
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: 'nmc',
|
||||
child: Text(l10n.appSettings_batteryNmc),
|
||||
const SizedBox(height: 12),
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: chemistry,
|
||||
isExpanded: true,
|
||||
decoration: const InputDecoration(
|
||||
border: UnderlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'lifepo4',
|
||||
child: Text(l10n.appSettings_batteryLifepo4),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'lipo',
|
||||
child: Text(l10n.appSettings_batteryLipo),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
settingsService.setBatteryChemistryForRepeater(
|
||||
repeater.publicKeyHex,
|
||||
value,
|
||||
);
|
||||
},
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: 'nmc',
|
||||
child: Text(l10n.appSettings_batteryNmc),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'lifepo4',
|
||||
child: Text(l10n.appSettings_batteryLifepo4),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'lipo',
|
||||
child: Text(l10n.appSettings_batteryLipo),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
l10n.repeater_managementTools,
|
||||
isAdmin
|
||||
? l10n.repeater_managementTools
|
||||
: l10n.repeater_guestTools,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -205,32 +217,32 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
TelemetryScreen(repeater: repeater, password: password),
|
||||
builder: (context) => TelemetryScreen(contact: repeater),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (isAdmin) const SizedBox(height: 12),
|
||||
// CLI button
|
||||
_buildManagementCard(
|
||||
context,
|
||||
icon: Icons.terminal,
|
||||
title: l10n.repeater_cli,
|
||||
subtitle: l10n.repeater_cliSubtitle,
|
||||
color: Colors.green,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => RepeaterCliScreen(
|
||||
repeater: repeater,
|
||||
password: password,
|
||||
if (isAdmin)
|
||||
_buildManagementCard(
|
||||
context,
|
||||
icon: Icons.terminal,
|
||||
title: l10n.repeater_cli,
|
||||
subtitle: l10n.repeater_cliSubtitle,
|
||||
color: Colors.green,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => RepeaterCliScreen(
|
||||
repeater: repeater,
|
||||
password: password,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Neighbors button
|
||||
_buildManagementCard(
|
||||
@@ -249,26 +261,27 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (isAdmin) const SizedBox(height: 12),
|
||||
// Settings button
|
||||
_buildManagementCard(
|
||||
context,
|
||||
icon: Icons.settings,
|
||||
title: l10n.repeater_settings,
|
||||
subtitle: l10n.repeater_settingsSubtitle,
|
||||
color: Colors.deepOrange,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => RepeaterSettingsScreen(
|
||||
repeater: repeater,
|
||||
password: password,
|
||||
if (isAdmin)
|
||||
_buildManagementCard(
|
||||
context,
|
||||
icon: Icons.settings,
|
||||
title: l10n.repeater_settings,
|
||||
subtitle: l10n.repeater_settingsSubtitle,
|
||||
color: Colors.deepOrange,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => RepeaterSettingsScreen(
|
||||
repeater: repeater,
|
||||
password: password,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -8,7 +8,9 @@ import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../services/app_debug_log_service.dart';
|
||||
import '../services/repeater_command_service.dart';
|
||||
import '../services/storage_service.dart';
|
||||
import '../widgets/path_management_dialog.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
class RepeaterSettingsScreen extends StatefulWidget {
|
||||
final Contact repeater;
|
||||
@@ -25,6 +27,8 @@ class RepeaterSettingsScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
final StorageService _storage = StorageService();
|
||||
|
||||
bool _isLoading = false;
|
||||
bool _hasChanges = false;
|
||||
bool _refreshingBasic = false;
|
||||
@@ -59,6 +63,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
bool _repeatEnabled = true;
|
||||
bool _allowReadOnly = true;
|
||||
bool _privacyMode = false;
|
||||
bool _autoClockSyncAfterLogin = false;
|
||||
|
||||
// Advertisement settings
|
||||
bool _advertEnable = true;
|
||||
@@ -129,11 +134,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) {
|
||||
@@ -453,18 +469,16 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
|
||||
if (mounted) {
|
||||
if (successCount > 0) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.repeater_refreshed(label)),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.repeater_refreshed(label)),
|
||||
backgroundColor: Colors.green,
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.repeater_errorRefreshing(label)),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.repeater_errorRefreshing(label)),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -555,6 +569,15 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
_lonController.text = widget.repeater.longitude?.toString() ?? '';
|
||||
}
|
||||
});
|
||||
|
||||
final autoClockSync = await _storage
|
||||
.getRepeaterAutoClockSyncAfterLoginEnabled(
|
||||
widget.repeater.publicKeyHex,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_autoClockSyncAfterLogin = autoClockSync;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _saveSettings() async {
|
||||
@@ -642,11 +665,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.repeater_settingsSaved),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.repeater_settingsSaved),
|
||||
backgroundColor: Colors.green,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -655,13 +677,12 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.repeater_errorSavingSettings(e.toString()),
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.repeater_errorSavingSettings(e.toString()),
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1128,6 +1149,21 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
onRefresh: _refreshAllowReadOnly,
|
||||
refreshTooltip: l10n.repeater_refreshGuestAccess,
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(l10n.repeater_clockSyncAfterLogin),
|
||||
subtitle: Text(l10n.repeater_clockSyncAfterLoginSubtitle),
|
||||
value: _autoClockSyncAfterLogin,
|
||||
onChanged: (value) async {
|
||||
setState(() {
|
||||
_autoClockSyncAfterLogin = value;
|
||||
});
|
||||
await _storage.setRepeaterAutoClockSyncAfterLoginEnabled(
|
||||
widget.repeater.publicKeyHex,
|
||||
value,
|
||||
);
|
||||
},
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
// Privacy mode - hidden until fully implemented
|
||||
// _buildFeatureToggleRow(
|
||||
// title: l10n.repeater_privacyMode,
|
||||
@@ -1390,9 +1426,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
|
||||
if (command == 'erase') {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.repeater_eraseSerialOnly)));
|
||||
content: Text(l10n.repeater_eraseSerialOnly),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1414,17 +1451,17 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
await connector.sendFrame(frame);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.repeater_commandSent(command))),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.repeater_commandSent(command)),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.repeater_errorSendingCommand(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.repeater_errorSendingCommand(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import '../services/app_settings_service.dart';
|
||||
import '../services/repeater_command_service.dart';
|
||||
import '../utils/battery_utils.dart';
|
||||
import '../widgets/path_management_dialog.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
class RepeaterStatusScreen extends StatefulWidget {
|
||||
final Contact repeater;
|
||||
@@ -91,11 +92,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) {
|
||||
@@ -298,11 +310,10 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.repeater_statusRequestTimeout),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.repeater_statusRequestTimeout),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
_recordStatusResult(false);
|
||||
});
|
||||
@@ -312,13 +323,10 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
_isLoading = false;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.repeater_errorLoadingStatus(e.toString()),
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.repeater_errorLoadingStatus(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
_recordStatusResult(false);
|
||||
|
||||
+233
-52
@@ -1,15 +1,19 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../utils/platform_info.dart';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../services/linux_ble_error_classifier.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../widgets/device_tile.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'contacts_screen.dart';
|
||||
import 'tcp_screen.dart';
|
||||
import 'usb_screen.dart';
|
||||
|
||||
/// Screen for scanning and connecting to MeshCore devices
|
||||
class ScannerScreen extends StatefulWidget {
|
||||
@@ -21,6 +25,7 @@ class ScannerScreen extends StatefulWidget {
|
||||
|
||||
class _ScannerScreenState extends State<ScannerScreen> {
|
||||
bool _changedNavigation = false;
|
||||
late final MeshCoreConnector _connector;
|
||||
late final VoidCallback _connectionListener;
|
||||
BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown;
|
||||
late StreamSubscription<BluetoothAdapterState> _bluetoothStateSubscription;
|
||||
@@ -28,12 +33,15 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
_connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
|
||||
_connectionListener = () {
|
||||
if (connector.state == MeshCoreConnectionState.disconnected) {
|
||||
final isCurrentRoute = ModalRoute.of(context)?.isCurrent ?? true;
|
||||
if (_connector.state == MeshCoreConnectionState.disconnected) {
|
||||
_changedNavigation = false;
|
||||
} else if (connector.state == MeshCoreConnectionState.connected &&
|
||||
} else if (_connector.state == MeshCoreConnectionState.connected &&
|
||||
_connector.activeTransport == MeshCoreTransportType.bluetooth &&
|
||||
isCurrentRoute &&
|
||||
!_changedNavigation) {
|
||||
_changedNavigation = true;
|
||||
if (mounted) {
|
||||
@@ -44,33 +52,52 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
}
|
||||
};
|
||||
|
||||
connector.addListener(_connectionListener);
|
||||
_connector.addListener(_connectionListener);
|
||||
|
||||
_bluetoothStateSubscription = FlutterBluePlus.adapterState.listen((state) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_bluetoothState = state;
|
||||
});
|
||||
// Cancel scan if Bluetooth turns off while scanning
|
||||
if (state != BluetoothAdapterState.on) {
|
||||
unawaited(connector.stopScan());
|
||||
_bluetoothStateSubscription = FlutterBluePlus.adapterState.listen(
|
||||
(state) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_bluetoothState = state;
|
||||
});
|
||||
// Cancel scan if Bluetooth turns off while scanning
|
||||
if (state != BluetoothAdapterState.on) {
|
||||
unawaited(_connector.stopScan());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
onError: (Object e) {
|
||||
appLogger.warn('Adapter state stream error: $e', tag: 'ScannerScreen');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
connector.removeListener(_connectionListener);
|
||||
_connector.removeListener(_connectionListener);
|
||||
unawaited(_bluetoothStateSubscription.cancel());
|
||||
if (!_changedNavigation) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
unawaited(_connector.disconnect(manual: true));
|
||||
});
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final canPop = Navigator.of(context).canPop();
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: canPop
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
appLogger.info('Back button pressed', tag: 'ScannerScreen');
|
||||
Navigator.of(context).maybePop();
|
||||
},
|
||||
)
|
||||
: null,
|
||||
title: AdaptiveAppBarTitle(context.l10n.scanner_title),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
@@ -95,36 +122,84 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
},
|
||||
),
|
||||
),
|
||||
floatingActionButton: Consumer<MeshCoreConnector>(
|
||||
bottomNavigationBar: Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, child) {
|
||||
final isScanning =
|
||||
connector.state == MeshCoreConnectionState.scanning;
|
||||
final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off;
|
||||
final usbSupported = PlatformInfo.supportsUsbSerial;
|
||||
final tcpSupported = !PlatformInfo.isWeb;
|
||||
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: isBluetoothOff
|
||||
? null
|
||||
: () {
|
||||
if (isScanning) {
|
||||
connector.stopScan();
|
||||
} else {
|
||||
connector.startScan();
|
||||
}
|
||||
},
|
||||
icon: isScanning
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
return SafeArea(
|
||||
top: false,
|
||||
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerRight,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (usbSupported)
|
||||
FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
appLogger.info(
|
||||
'USB selected, opening UsbScreen',
|
||||
tag: 'ScannerScreen',
|
||||
);
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const UsbScreen()),
|
||||
);
|
||||
},
|
||||
heroTag: 'scanner_usb_action',
|
||||
icon: const Icon(Icons.usb),
|
||||
label: Text(context.l10n.connectionChoiceUsbLabel),
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.bluetooth_searching),
|
||||
label: Text(
|
||||
isScanning
|
||||
? context.l10n.scanner_stop
|
||||
: context.l10n.scanner_scan,
|
||||
if (usbSupported) const SizedBox(width: 12),
|
||||
if (tcpSupported)
|
||||
FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const TcpScreen()),
|
||||
);
|
||||
},
|
||||
heroTag: 'scanner_tcp_action',
|
||||
icon: const Icon(Icons.lan),
|
||||
label: Text(context.l10n.connectionChoiceTcpLabel),
|
||||
),
|
||||
if (tcpSupported) const SizedBox(width: 12),
|
||||
FloatingActionButton.extended(
|
||||
heroTag: 'scanner_ble_action',
|
||||
onPressed: isBluetoothOff
|
||||
? null
|
||||
: () {
|
||||
if (isScanning) {
|
||||
connector.stopScan();
|
||||
} else {
|
||||
unawaited(
|
||||
connector.startScan().catchError((e) {
|
||||
appLogger.warn(
|
||||
'startScan error: $e',
|
||||
tag: 'ScannerScreen',
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: isScanning
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.bluetooth_searching),
|
||||
label: Text(
|
||||
isScanning
|
||||
? context.l10n.scanner_stop
|
||||
: context.l10n.scanner_scan,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -215,23 +290,129 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
MeshCoreConnector connector,
|
||||
ScanResult result,
|
||||
) async {
|
||||
final name = result.device.platformName.isNotEmpty
|
||||
? result.device.platformName
|
||||
: result.advertisementData.advName;
|
||||
try {
|
||||
final name = result.device.platformName.isNotEmpty
|
||||
? result.device.platformName
|
||||
: result.advertisementData.advName;
|
||||
await connector.connect(result.device, displayName: name);
|
||||
await connector.connect(
|
||||
result.device,
|
||||
displayName: name,
|
||||
linuxPairingPinProvider: PlatformInfo.isLinux
|
||||
? () async {
|
||||
if (!context.mounted) return null;
|
||||
return _promptLinuxPairingPin(context, name);
|
||||
}
|
||||
: null,
|
||||
);
|
||||
} catch (e) {
|
||||
final errorText = e.toString();
|
||||
final suppressTransientLinuxConnectError =
|
||||
PlatformInfo.isLinux &&
|
||||
connector.isAutoReconnectScheduled &&
|
||||
isLinuxBleConnectFailureText(errorText);
|
||||
if (suppressTransientLinuxConnectError) {
|
||||
appLogger.info(
|
||||
'Suppressing transient Linux connect error while auto-reconnect is active: $e',
|
||||
tag: 'ScannerScreen',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.scanner_connectionFailed(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.scanner_connectionFailed(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _promptLinuxPairingPin(
|
||||
BuildContext context,
|
||||
String deviceName,
|
||||
) async {
|
||||
final l10n = context.l10n;
|
||||
var pinValue = '';
|
||||
var obscure = true;
|
||||
appLogger.info(
|
||||
'Showing Linux BLE pairing PIN prompt for $deviceName',
|
||||
tag: 'ScannerScreen',
|
||||
);
|
||||
final pin = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return StatefulBuilder(
|
||||
builder: (dialogContext, setDialogState) {
|
||||
return AlertDialog(
|
||||
title: Text(l10n.scanner_linuxPairingPinTitle),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.scanner_linuxPairingPinPrompt(deviceName)),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
autofocus: true,
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.done,
|
||||
obscureText: obscure,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
onChanged: (value) {
|
||||
pinValue = value.trim();
|
||||
},
|
||||
onSubmitted: (value) {
|
||||
Navigator.of(dialogContext).pop(value.trim());
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
setDialogState(() {
|
||||
obscure = !obscure;
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
obscure ? Icons.visibility : Icons.visibility_off,
|
||||
),
|
||||
tooltip: obscure
|
||||
? l10n.scanner_linuxPairingShowPin
|
||||
: l10n.scanner_linuxPairingHidePin,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(null),
|
||||
child: Text(l10n.common_cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(pinValue),
|
||||
child: Text(l10n.common_connect),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
if (pin == null) {
|
||||
appLogger.info(
|
||||
'Linux BLE pairing PIN prompt cancelled for $deviceName',
|
||||
tag: 'ScannerScreen',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
appLogger.info(
|
||||
'Linux BLE pairing PIN prompt completed for $deviceName',
|
||||
tag: 'ScannerScreen',
|
||||
);
|
||||
return pin;
|
||||
}
|
||||
|
||||
Widget _bluetoothOffWarning(BuildContext context) {
|
||||
final errorColor = Theme.of(context).colorScheme.error;
|
||||
return Container(
|
||||
@@ -265,7 +446,7 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (Platform.isAndroid)
|
||||
if (PlatformInfo.isAndroid)
|
||||
TextButton(
|
||||
onPressed: () => FlutterBluePlus.turnOn(),
|
||||
child: Text(context.l10n.scanner_enableBluetooth),
|
||||
|
||||
+728
-117
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,298 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../utils/platform_info.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'contacts_screen.dart';
|
||||
import 'usb_screen.dart';
|
||||
|
||||
class TcpScreen extends StatefulWidget {
|
||||
const TcpScreen({super.key});
|
||||
|
||||
@override
|
||||
State<TcpScreen> createState() => _TcpScreenState();
|
||||
}
|
||||
|
||||
class _TcpScreenState extends State<TcpScreen> {
|
||||
late final TextEditingController _hostController;
|
||||
late final TextEditingController _portController;
|
||||
late final MeshCoreConnector _connector;
|
||||
late final VoidCallback _connectionListener;
|
||||
bool _navigatedToContacts = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_hostController = TextEditingController(
|
||||
text: context.read<AppSettingsService>().settings.tcpServerAddress,
|
||||
);
|
||||
_portController = TextEditingController(
|
||||
text: context.read<AppSettingsService>().settings.tcpServerPort > 0
|
||||
? context.read<AppSettingsService>().settings.tcpServerPort.toString()
|
||||
: '',
|
||||
);
|
||||
_connector = context.read<MeshCoreConnector>();
|
||||
|
||||
_connectionListener = () {
|
||||
if (!mounted) return;
|
||||
if (_connector.state == MeshCoreConnectionState.disconnected) {
|
||||
_navigatedToContacts = false;
|
||||
}
|
||||
if (_connector.state == MeshCoreConnectionState.connected &&
|
||||
_connector.isTcpTransportConnected &&
|
||||
!_navigatedToContacts) {
|
||||
context.read<AppSettingsService>().setTcpServerAddress(
|
||||
_hostController.text,
|
||||
);
|
||||
context.read<AppSettingsService>().setTcpServerPort(
|
||||
int.tryParse(_portController.text) ?? 0,
|
||||
);
|
||||
_navigatedToContacts = true;
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const ContactsScreen()),
|
||||
);
|
||||
}
|
||||
};
|
||||
_connector.addListener(_connectionListener);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hostController.dispose();
|
||||
_portController.dispose();
|
||||
_connector.removeListener(_connectionListener);
|
||||
if (!_navigatedToContacts &&
|
||||
_connector.activeTransport == MeshCoreTransportType.tcp &&
|
||||
_connector.state != MeshCoreConnectionState.disconnected) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
unawaited(_connector.disconnect(manual: true));
|
||||
});
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
title: AdaptiveAppBarTitle(context.l10n.tcpScreenTitle),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, child) {
|
||||
final isConnecting =
|
||||
connector.state == MeshCoreConnectionState.connecting &&
|
||||
connector.activeTransport == MeshCoreTransportType.tcp;
|
||||
final isButtonDisabled =
|
||||
isConnecting ||
|
||||
connector.state == MeshCoreConnectionState.scanning;
|
||||
return Column(
|
||||
children: [
|
||||
_buildStatusBar(context, connector),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _hostController,
|
||||
decoration: InputDecoration(
|
||||
labelText: context.l10n.tcpHostLabel,
|
||||
hintText: context.l10n.tcpHostHint,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
enabled: !isConnecting,
|
||||
keyboardType: TextInputType.url,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _portController,
|
||||
decoration: InputDecoration(
|
||||
labelText: context.l10n.tcpPortLabel,
|
||||
hintText: context.l10n.tcpPortHint,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
enabled: !isConnecting,
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton.icon(
|
||||
key: const Key('tcp_connect_button'),
|
||||
onPressed: isButtonDisabled ? null : _connectTcp,
|
||||
icon: isConnecting
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.lan),
|
||||
label: Text(
|
||||
isConnecting
|
||||
? context.l10n.scanner_connecting
|
||||
: context.l10n.common_connect,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
top: false,
|
||||
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerRight,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (PlatformInfo.supportsUsbSerial)
|
||||
FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const UsbScreen()),
|
||||
);
|
||||
},
|
||||
heroTag: 'tcp_usb_action',
|
||||
extendedPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
icon: const Icon(Icons.usb),
|
||||
label: Text(context.l10n.connectionChoiceUsbLabel),
|
||||
),
|
||||
if (PlatformInfo.supportsUsbSerial) const SizedBox(width: 12),
|
||||
FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
Navigator.of(context).maybePop();
|
||||
},
|
||||
heroTag: 'tcp_ble_action',
|
||||
extendedPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
icon: const Icon(Icons.bluetooth),
|
||||
label: Text(context.l10n.connectionChoiceBluetoothLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusBar(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
String statusText;
|
||||
Color statusColor;
|
||||
|
||||
if (connector.isTcpTransportConnected) {
|
||||
statusText = l10n.scanner_connectedTo(
|
||||
connector.activeTcpEndpoint ?? 'TCP',
|
||||
);
|
||||
statusColor = Colors.green;
|
||||
} else if (connector.state == MeshCoreConnectionState.connecting &&
|
||||
connector.activeTransport == MeshCoreTransportType.tcp) {
|
||||
statusText = l10n.tcpStatus_connectingTo(
|
||||
'${_hostController.text}:${_portController.text}',
|
||||
);
|
||||
statusColor = Colors.orange;
|
||||
} else if (connector.state == MeshCoreConnectionState.disconnecting &&
|
||||
connector.activeTransport == MeshCoreTransportType.tcp) {
|
||||
statusText = l10n.scanner_disconnecting;
|
||||
statusColor = Colors.orange;
|
||||
} else {
|
||||
statusText = l10n.tcpStatus_notConnected;
|
||||
statusColor = Colors.grey;
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
color: statusColor.withValues(alpha: 0.1),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.circle, size: 12, color: statusColor),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _connectTcp() async {
|
||||
if (_connector.state == MeshCoreConnectionState.connecting ||
|
||||
_connector.state == MeshCoreConnectionState.connected ||
|
||||
_connector.state == MeshCoreConnectionState.disconnecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
final host = _hostController.text.trim();
|
||||
final parsedPort = int.tryParse(_portController.text.trim());
|
||||
if (host.isEmpty) {
|
||||
_showError(context.l10n.tcpErrorHostRequired);
|
||||
return;
|
||||
}
|
||||
if (parsedPort == null || parsedPort < 1 || parsedPort > 65535) {
|
||||
_showError(context.l10n.tcpErrorPortInvalid);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await _connector.connectTcp(host: host, port: parsedPort);
|
||||
} catch (error) {
|
||||
if (!mounted) return;
|
||||
_showError(_friendlyErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
void _showError(String message) {
|
||||
if (!mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
|
||||
String _friendlyErrorMessage(Object error) {
|
||||
if (error is UnsupportedError) {
|
||||
return context.l10n.tcpErrorUnsupported;
|
||||
}
|
||||
if (error is TimeoutException) {
|
||||
return context.l10n.tcpErrorTimedOut;
|
||||
}
|
||||
if (error is StateError) {
|
||||
return context.l10n.tcpConnectionFailed(error.message);
|
||||
}
|
||||
if (error is ArgumentError) {
|
||||
return context.l10n.tcpConnectionFailed(
|
||||
error.message?.toString() ?? error.toString(),
|
||||
);
|
||||
}
|
||||
return context.l10n.tcpConnectionFailed(error.toString());
|
||||
}
|
||||
}
|
||||
@@ -10,30 +10,23 @@ 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';
|
||||
import '../helpers/snack_bar_builder.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 +37,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 +73,61 @@ 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;
|
||||
});
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
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',
|
||||
);
|
||||
@@ -90,11 +137,10 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
_parsedTelemetry = parsedTelemetry;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.telemetry_receivedData),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.telemetry_receivedData),
|
||||
backgroundColor: Colors.green,
|
||||
);
|
||||
_statusTimeout?.cancel();
|
||||
if (!mounted) return;
|
||||
@@ -105,13 +151,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 +160,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(() {
|
||||
@@ -163,22 +181,25 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
_isLoaded = false;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.telemetry_errorLoading(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.telemetry_errorLoading(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 +217,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 +230,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 +245,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 +303,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 +457,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 +469,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
String _batteryChemistry() {
|
||||
final settingsService = context.read<AppSettingsService>();
|
||||
return settingsService.batteryChemistryForRepeater(
|
||||
widget.repeater.publicKeyHex,
|
||||
widget.contact.publicKeyHex,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,441 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../utils/platform_info.dart';
|
||||
import '../utils/usb_port_labels.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'contacts_screen.dart';
|
||||
import 'scanner_screen.dart';
|
||||
import 'tcp_screen.dart';
|
||||
|
||||
class UsbScreen extends StatefulWidget {
|
||||
const UsbScreen({super.key});
|
||||
|
||||
@override
|
||||
State<UsbScreen> createState() => _UsbScreenState();
|
||||
}
|
||||
|
||||
class _UsbScreenState extends State<UsbScreen> {
|
||||
final List<String> _ports = <String>[];
|
||||
bool _isLoadingPorts = true;
|
||||
bool _navigatedToContacts = false;
|
||||
bool _didScheduleInitialLoad = false;
|
||||
Timer? _hotPlugTimer;
|
||||
late final MeshCoreConnector _connector;
|
||||
late final VoidCallback _connectionListener;
|
||||
|
||||
bool get _supportsHotPlug =>
|
||||
PlatformInfo.isWindows || PlatformInfo.isLinux || PlatformInfo.isMacOS;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_connector = context.read<MeshCoreConnector>();
|
||||
_connectionListener = () {
|
||||
if (!mounted) return;
|
||||
if (_connector.state == MeshCoreConnectionState.disconnected) {
|
||||
_navigatedToContacts = false;
|
||||
}
|
||||
if (_connector.state == MeshCoreConnectionState.connected &&
|
||||
_connector.isUsbTransportConnected &&
|
||||
!_navigatedToContacts) {
|
||||
_navigatedToContacts = true;
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const ContactsScreen()),
|
||||
);
|
||||
}
|
||||
};
|
||||
_connector.addListener(_connectionListener);
|
||||
_startHotPlugTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus);
|
||||
_connector.setUsbFallbackDeviceName(context.l10n.usbFallbackDeviceName);
|
||||
if (!_didScheduleInitialLoad) {
|
||||
_didScheduleInitialLoad = true;
|
||||
unawaited(_loadPorts());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hotPlugTimer?.cancel();
|
||||
_hotPlugTimer = null;
|
||||
_connector.removeListener(_connectionListener);
|
||||
if (!_navigatedToContacts &&
|
||||
_connector.activeTransport == MeshCoreTransportType.usb &&
|
||||
_connector.state != MeshCoreConnectionState.disconnected) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
unawaited(_connector.disconnect(manual: true));
|
||||
});
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
title: AdaptiveAppBarTitle(context.l10n.usbScreenTitle),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, child) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildStatusBar(context, connector),
|
||||
Expanded(child: _buildPortList(context, connector)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, child) {
|
||||
final isLoading = _isLoadingPorts;
|
||||
final showBle = true;
|
||||
final showTcp = !PlatformInfo.isWeb;
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerRight,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (showTcp)
|
||||
FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const TcpScreen()),
|
||||
);
|
||||
},
|
||||
heroTag: 'usb_tcp_action',
|
||||
extendedPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
),
|
||||
icon: const Icon(Icons.lan),
|
||||
label: Text(context.l10n.connectionChoiceTcpLabel),
|
||||
),
|
||||
if (showTcp && showBle) const SizedBox(width: 12),
|
||||
if (showBle)
|
||||
FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const ScannerScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
heroTag: 'usb_ble_action',
|
||||
extendedPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
),
|
||||
icon: const Icon(Icons.bluetooth),
|
||||
label: Text(context.l10n.connectionChoiceBluetoothLabel),
|
||||
),
|
||||
if ((showTcp || showBle) && !_supportsHotPlug)
|
||||
const SizedBox(width: 12),
|
||||
if (!_supportsHotPlug)
|
||||
FloatingActionButton.extended(
|
||||
onPressed: isLoading ? null : _loadPorts,
|
||||
heroTag: 'usb_refresh_action',
|
||||
extendedPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
),
|
||||
icon: isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.usb),
|
||||
label: Text(context.l10n.scanner_scan),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusBar(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
String statusText;
|
||||
Color statusColor;
|
||||
|
||||
if (_isLoadingPorts) {
|
||||
statusText = l10n.usbStatus_searching;
|
||||
statusColor = Colors.blue;
|
||||
} else if (connector.isUsbTransportConnected) {
|
||||
switch (connector.state) {
|
||||
case MeshCoreConnectionState.connected:
|
||||
statusText = l10n.scanner_connectedTo(
|
||||
connector.activeUsbPortDisplayLabel ?? 'USB',
|
||||
);
|
||||
statusColor = Colors.green;
|
||||
case MeshCoreConnectionState.disconnecting:
|
||||
statusText = l10n.scanner_disconnecting;
|
||||
statusColor = Colors.orange;
|
||||
default:
|
||||
statusText = l10n.usbStatus_notConnected;
|
||||
statusColor = Colors.grey;
|
||||
}
|
||||
} else if (connector.state == MeshCoreConnectionState.connecting &&
|
||||
connector.activeTransport == MeshCoreTransportType.usb) {
|
||||
statusText = l10n.usbStatus_connecting;
|
||||
statusColor = Colors.orange;
|
||||
} else {
|
||||
statusText = l10n.usbStatus_notConnected;
|
||||
statusColor = Colors.grey;
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
color: statusColor.withValues(alpha: 0.1),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.circle, size: 12, color: statusColor),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPortList(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
|
||||
if (_isLoadingPorts) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.usb, size: 64, color: Colors.grey[400]),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.usbStatus_searching,
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_ports.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.usb, size: 64, color: Colors.grey[400]),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.usbScreenEmptyState,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final isConnecting =
|
||||
connector.state == MeshCoreConnectionState.connecting &&
|
||||
connector.activeTransport == MeshCoreTransportType.usb;
|
||||
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.all(8),
|
||||
itemCount: _ports.length,
|
||||
separatorBuilder: (context, index) => const Divider(),
|
||||
itemBuilder: (context, index) {
|
||||
final port = _ports[index];
|
||||
final displayName = friendlyUsbPortName(port);
|
||||
final rawName = normalizeUsbPortName(port);
|
||||
final showRawName =
|
||||
rawName != displayName && !rawName.startsWith('web:');
|
||||
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.usb),
|
||||
title: Text(
|
||||
displayName,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
subtitle: showRawName ? Text(rawName) : null,
|
||||
trailing: ElevatedButton(
|
||||
onPressed: isConnecting ? null : () => _connectPort(port),
|
||||
child: Text(l10n.common_connect),
|
||||
),
|
||||
onTap: isConnecting ? null : () => _connectPort(port),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _startHotPlugTimer() {
|
||||
if (!_supportsHotPlug) return;
|
||||
_hotPlugTimer?.cancel();
|
||||
_hotPlugTimer = Timer.periodic(const Duration(seconds: 2), (_) {
|
||||
_pollHotPlug();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _pollHotPlug() async {
|
||||
if (_isLoadingPorts) return;
|
||||
if (!mounted) return;
|
||||
// Don't poll while connecting or connected.
|
||||
if (_connector.state != MeshCoreConnectionState.disconnected) return;
|
||||
try {
|
||||
final ports = await _connector.listUsbPorts();
|
||||
if (!mounted) return;
|
||||
final added = ports.where((p) => !_ports.contains(p)).toList();
|
||||
final removed = _ports.where((p) => !ports.contains(p)).toList();
|
||||
if (added.isEmpty && removed.isEmpty) return;
|
||||
setState(() {
|
||||
_ports
|
||||
..clear()
|
||||
..addAll(ports);
|
||||
});
|
||||
} catch (_) {
|
||||
// Silent — hot-plug failures are non-critical.
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadPorts() async {
|
||||
if (!mounted) return;
|
||||
_connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus);
|
||||
|
||||
setState(() {
|
||||
_isLoadingPorts = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final ports = await _connector.listUsbPorts();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_ports
|
||||
..clear()
|
||||
..addAll(ports);
|
||||
_isLoadingPorts = false;
|
||||
});
|
||||
} catch (error) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_ports.clear();
|
||||
_isLoadingPorts = false;
|
||||
});
|
||||
_showError(error);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _connectPort(String port) async {
|
||||
if (_connector.state != MeshCoreConnectionState.disconnected) return;
|
||||
|
||||
final rawPortName = normalizeUsbPortName(port);
|
||||
appLogger.info(
|
||||
'Connect tapped for $port (raw: $rawPortName)',
|
||||
tag: 'UsbScreen',
|
||||
);
|
||||
|
||||
try {
|
||||
await _connector.connectUsb(portName: rawPortName);
|
||||
} catch (error, stackTrace) {
|
||||
appLogger.error(
|
||||
'Connect failed for $rawPortName: $error\n$stackTrace',
|
||||
tag: 'UsbScreen',
|
||||
);
|
||||
if (!mounted) return;
|
||||
_showError(error);
|
||||
unawaited(_loadPorts());
|
||||
}
|
||||
}
|
||||
|
||||
void _showError(Object error) {
|
||||
if (!mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(_friendlyErrorMessage(error)),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
|
||||
String _friendlyErrorMessage(Object error) {
|
||||
final l10n = context.l10n;
|
||||
|
||||
if (error is PlatformException) {
|
||||
switch (error.code) {
|
||||
case 'usb_permission_denied':
|
||||
return l10n.usbErrorPermissionDenied;
|
||||
case 'usb_device_missing':
|
||||
case 'usb_device_detached':
|
||||
return l10n.usbErrorDeviceMissing;
|
||||
case 'usb_invalid_port':
|
||||
return l10n.usbErrorInvalidPort;
|
||||
case 'usb_busy':
|
||||
return l10n.usbErrorBusy;
|
||||
case 'usb_not_connected':
|
||||
return l10n.usbErrorNotConnected;
|
||||
case 'usb_open_failed':
|
||||
case 'usb_driver_missing':
|
||||
return l10n.usbErrorOpenFailed;
|
||||
case 'usb_connect_failed':
|
||||
return l10n.usbErrorConnectFailed;
|
||||
}
|
||||
}
|
||||
|
||||
if (error is UnsupportedError) {
|
||||
return l10n.usbErrorUnsupported;
|
||||
}
|
||||
|
||||
if (error is StateError) {
|
||||
final msg = error.message;
|
||||
if (msg.contains('already active')) return l10n.usbErrorAlreadyActive;
|
||||
if (msg.contains('No USB serial device selected')) {
|
||||
return l10n.usbErrorNoDeviceSelected;
|
||||
}
|
||||
if (msg.contains('not open') || msg.contains('closed')) {
|
||||
return l10n.usbErrorPortClosed;
|
||||
}
|
||||
if (msg.contains('Timed out')) return l10n.usbErrorConnectTimedOut;
|
||||
if (msg.contains('Failed to open')) return l10n.usbErrorOpenFailed;
|
||||
}
|
||||
|
||||
if (error is TimeoutException) {
|
||||
return l10n.usbErrorConnectTimedOut;
|
||||
}
|
||||
|
||||
return error.toString();
|
||||
}
|
||||
}
|
||||
@@ -51,8 +51,14 @@ class AppDebugLogService extends ChangeNotifier {
|
||||
String message, {
|
||||
String tag = 'App',
|
||||
AppDebugLogLevel level = AppDebugLogLevel.info,
|
||||
bool noNotify = false,
|
||||
}) {
|
||||
if (!_enabled) return;
|
||||
if (!_enabled && !kDebugMode) return;
|
||||
if (!_enabled) {
|
||||
// In debug mode, still print to console but don't store entries.
|
||||
debugPrint('[$tag] $message');
|
||||
return;
|
||||
}
|
||||
|
||||
_entries.add(
|
||||
AppDebugLogEntry(
|
||||
@@ -67,22 +73,24 @@ class AppDebugLogService extends ChangeNotifier {
|
||||
_entries.removeRange(0, _entries.length - maxEntries);
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
if (!noNotify) {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Also print to console for development
|
||||
debugPrint('[$tag] $message');
|
||||
}
|
||||
|
||||
void info(String message, {String tag = 'App'}) {
|
||||
log(message, tag: tag, level: AppDebugLogLevel.info);
|
||||
void info(String message, {String tag = 'App', bool noNotify = false}) {
|
||||
log(message, tag: tag, level: AppDebugLogLevel.info, noNotify: noNotify);
|
||||
}
|
||||
|
||||
void warn(String message, {String tag = 'App'}) {
|
||||
log(message, tag: tag, level: AppDebugLogLevel.warning);
|
||||
void warn(String message, {String tag = 'App', bool noNotify = false}) {
|
||||
log(message, tag: tag, level: AppDebugLogLevel.warning, noNotify: noNotify);
|
||||
}
|
||||
|
||||
void error(String message, {String tag = 'App'}) {
|
||||
log(message, tag: tag, level: AppDebugLogLevel.error);
|
||||
void error(String message, {String tag = 'App', bool noNotify = false}) {
|
||||
log(message, tag: tag, level: AppDebugLogLevel.error, noNotify: noNotify);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../models/app_settings.dart';
|
||||
import '../models/translation_support.dart';
|
||||
import '../storage/prefs_manager.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
|
||||
@@ -64,6 +65,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));
|
||||
}
|
||||
@@ -80,6 +85,10 @@ class AppSettingsService extends ChangeNotifier {
|
||||
await updateSettings(_settings.copyWith(mapShowMarkers: value));
|
||||
}
|
||||
|
||||
Future<void> setMapShowGuessedLocations(bool value) async {
|
||||
await updateSettings(_settings.copyWith(mapShowGuessedLocations: value));
|
||||
}
|
||||
|
||||
Future<void> setEnableMessageTracing(bool value) async {
|
||||
await updateSettings(_settings.copyWith(enableMessageTracing: value));
|
||||
}
|
||||
@@ -116,6 +125,30 @@ class AppSettingsService extends ChangeNotifier {
|
||||
await updateSettings(_settings.copyWith(autoRouteRotationEnabled: value));
|
||||
}
|
||||
|
||||
Future<void> setMaxRouteWeight(double value) async {
|
||||
await updateSettings(_settings.copyWith(maxRouteWeight: value));
|
||||
}
|
||||
|
||||
Future<void> setInitialRouteWeight(double value) async {
|
||||
await updateSettings(_settings.copyWith(initialRouteWeight: value));
|
||||
}
|
||||
|
||||
Future<void> setRouteWeightSuccessIncrement(double value) async {
|
||||
await updateSettings(
|
||||
_settings.copyWith(routeWeightSuccessIncrement: value),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setRouteWeightFailureDecrement(double value) async {
|
||||
await updateSettings(
|
||||
_settings.copyWith(routeWeightFailureDecrement: value),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setMaxMessageRetries(int value) async {
|
||||
await updateSettings(_settings.copyWith(maxMessageRetries: value));
|
||||
}
|
||||
|
||||
Future<void> setThemeMode(String value) async {
|
||||
await updateSettings(_settings.copyWith(themeMode: value));
|
||||
}
|
||||
@@ -130,6 +163,10 @@ class AppSettingsService extends ChangeNotifier {
|
||||
appLogger.setEnabled(value);
|
||||
}
|
||||
|
||||
Future<void> setMapShowDiscoveryContacts(bool value) async {
|
||||
await updateSettings(_settings.copyWith(mapShowDiscoveryContacts: value));
|
||||
}
|
||||
|
||||
Future<void> setBatteryChemistryForDevice(
|
||||
String deviceId,
|
||||
String chemistry,
|
||||
@@ -174,4 +211,46 @@ class AppSettingsService extends ChangeNotifier {
|
||||
..remove(channelName);
|
||||
await updateSettings(_settings.copyWith(mutedChannels: updated));
|
||||
}
|
||||
|
||||
Future<void> setTcpServerAddress(String value) async {
|
||||
await updateSettings(_settings.copyWith(tcpServerAddress: value));
|
||||
}
|
||||
|
||||
Future<void> setTcpServerPort(int value) async {
|
||||
await updateSettings(_settings.copyWith(tcpServerPort: value));
|
||||
}
|
||||
|
||||
Future<void> setJumpToOldestUnread(bool value) async {
|
||||
await updateSettings(_settings.copyWith(jumpToOldestUnread: value));
|
||||
}
|
||||
|
||||
Future<void> setTranslationEnabled(bool value) async {
|
||||
await updateSettings(_settings.copyWith(translationEnabled: value));
|
||||
}
|
||||
|
||||
Future<void> setTranslationTargetLanguageCode(String? value) async {
|
||||
await updateSettings(
|
||||
_settings.copyWith(translationTargetLanguageCode: value),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setComposerTranslationEnabled(bool value) async {
|
||||
await updateSettings(_settings.copyWith(composerTranslationEnabled: value));
|
||||
}
|
||||
|
||||
Future<void> setTranslationModelSourceUrl(String? value) async {
|
||||
await updateSettings(_settings.copyWith(translationModelSourceUrl: value));
|
||||
}
|
||||
|
||||
Future<void> setTranslationSelectedModelId(String? value) async {
|
||||
await updateSettings(_settings.copyWith(translationSelectedModelId: value));
|
||||
}
|
||||
|
||||
Future<void> setTranslationDownloadedModels(
|
||||
List<TranslationModelRecord> value,
|
||||
) async {
|
||||
await updateSettings(
|
||||
_settings.copyWith(translationDownloadedModels: value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import 'dart:io';
|
||||
|
||||
import '../utils/platform_info.dart';
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
|
||||
class BackgroundService {
|
||||
bool _initialized = false;
|
||||
|
||||
Future<void> initialize() async {
|
||||
if (!Platform.isAndroid || _initialized) return;
|
||||
if (!PlatformInfo.isAndroid || _initialized) return;
|
||||
FlutterForegroundTask.init(
|
||||
androidNotificationOptions: AndroidNotificationOptions(
|
||||
channelId: 'meshcore_background',
|
||||
@@ -29,7 +28,7 @@ class BackgroundService {
|
||||
}
|
||||
|
||||
Future<void> start() async {
|
||||
if (!Platform.isAndroid) return;
|
||||
if (!PlatformInfo.isAndroid) return;
|
||||
if (!_initialized) {
|
||||
await initialize();
|
||||
}
|
||||
@@ -43,7 +42,7 @@ class BackgroundService {
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
if (!Platform.isAndroid) return;
|
||||
if (!PlatformInfo.isAndroid) return;
|
||||
final running = await FlutterForegroundTask.isRunningService;
|
||||
if (!running) return;
|
||||
await FlutterForegroundTask.stopService();
|
||||
|
||||
@@ -172,8 +172,6 @@ class BleDebugLogService extends ChangeNotifier {
|
||||
return 'CMD_GET_CHANNEL';
|
||||
case cmdSetChannel:
|
||||
return 'CMD_SET_CHANNEL';
|
||||
case cmdGetRadioSettings:
|
||||
return 'CMD_GET_RADIO_SETTINGS';
|
||||
case cmdSetCustomVar:
|
||||
return 'CMD_SET_CUSTOM_VAR';
|
||||
case cmdSendTracePath:
|
||||
@@ -215,8 +213,8 @@ class BleDebugLogService extends ChangeNotifier {
|
||||
return 'RESP_CODE_CHANNEL_MSG_RECV_V3';
|
||||
case respCodeChannelInfo:
|
||||
return 'RESP_CODE_CHANNEL_INFO';
|
||||
case respCodeRadioSettings:
|
||||
return 'RESP_CODE_RADIO_SETTINGS';
|
||||
case respCodeAutoAddConfig:
|
||||
return 'RESP_CODE_AUTO_ADD_CONFIG';
|
||||
case pushCodeTraceData:
|
||||
return 'PUSH_CODE_TRACE_DATA';
|
||||
default:
|
||||
|
||||
@@ -65,7 +65,7 @@ class ChatTextScaleService extends ChangeNotifier {
|
||||
|
||||
void _commitScale() {
|
||||
_saveTimer?.cancel();
|
||||
PrefsManager.instance.setDouble(_prefKey, _scale);
|
||||
unawaited(PrefsManager.instance.setDouble(_prefKey, _scale));
|
||||
}
|
||||
|
||||
double _clamp(double value) => value.clamp(_minScale, _maxScale).toDouble();
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
const String linuxConnectStageFailureMarker = 'linux connect stage failure';
|
||||
|
||||
bool isLinuxBleConnectFailureText(String errorText) {
|
||||
final lowerErrorText = errorText.toLowerCase();
|
||||
if (isLinuxBlePairingFailureText(errorText)) {
|
||||
return false;
|
||||
}
|
||||
return lowerErrorText.contains(linuxConnectStageFailureMarker) ||
|
||||
lowerErrorText.contains('| connect |') ||
|
||||
lowerErrorText.contains('linux connect hard-timeout') ||
|
||||
lowerErrorText.contains('org.bluez.error.failed') ||
|
||||
lowerErrorText.contains('org.bluez.error.inprogress') ||
|
||||
lowerErrorText.contains('le-connection-abort-by-local');
|
||||
}
|
||||
|
||||
bool isLinuxBlePairingFailureText(String errorText) {
|
||||
final lowerErrorText = errorText.toLowerCase();
|
||||
final isPairingSpecificStateError =
|
||||
lowerErrorText.contains('bad state: no element') &&
|
||||
(lowerErrorText.contains('pair') ||
|
||||
lowerErrorText.contains('bond') ||
|
||||
lowerErrorText.contains('trust'));
|
||||
return lowerErrorText.contains('authenticationfailed') ||
|
||||
lowerErrorText.contains('authentication failed') ||
|
||||
lowerErrorText.contains('notpermitted: not paired') ||
|
||||
lowerErrorText.contains('pairing fallback failed') ||
|
||||
lowerErrorText.contains('linux ble pairing did not complete') ||
|
||||
lowerErrorText.contains('linux ble trust repair did not complete') ||
|
||||
isPairingSpecificStateError ||
|
||||
isLikelyLinuxBlePairingTimeoutText(errorText);
|
||||
}
|
||||
|
||||
bool isLikelyLinuxBlePairingTimeoutText(String errorText) {
|
||||
final lowerErrorText = errorText.toLowerCase();
|
||||
return lowerErrorText.contains('timed out') &&
|
||||
(lowerErrorText.contains('pair') || lowerErrorText.contains('bond'));
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
typedef ProcessStartFn =
|
||||
Future<Process> Function(String executable, List<String> arguments);
|
||||
typedef ProcessRunFn =
|
||||
Future<ProcessResult> Function(String executable, List<String> arguments);
|
||||
|
||||
/// Best-effort Linux BLE pairing helper using bluetoothctl.
|
||||
///
|
||||
/// This is used only as a fallback when BlueZ pairing via flutter_blue_plus
|
||||
/// fails to surface agent prompts in-app.
|
||||
class LinuxBlePairingService {
|
||||
/// Maximum number of pairing attempts (initial + retries).
|
||||
/// Covers one remove-and-retry plus one proactive-PIN retry.
|
||||
static const int _maxAttempts = 3;
|
||||
|
||||
static const Duration _processExitTimeout = Duration(seconds: 6);
|
||||
static const Duration _pairingCleanupTimeout = Duration(seconds: 5);
|
||||
static const Duration _defaultPairingTimeout = Duration(seconds: 45);
|
||||
LinuxBlePairingService({
|
||||
ProcessStartFn? processStart,
|
||||
ProcessRunFn? processRun,
|
||||
}) : _processStart = processStart ?? Process.start,
|
||||
_processRun = processRun ?? Process.run;
|
||||
|
||||
final ProcessStartFn _processStart;
|
||||
final ProcessRunFn _processRun;
|
||||
|
||||
Future<bool> isBluetoothctlAvailable() async {
|
||||
try {
|
||||
final result = await _processRun('bluetoothctl', <String>['--version']);
|
||||
return result.exitCode == 0;
|
||||
} on ProcessException {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> disconnectDevice(
|
||||
String remoteId, {
|
||||
void Function(String message)? onLog,
|
||||
}) async {
|
||||
onLog?.call('Requesting BlueZ disconnect for $remoteId');
|
||||
Process process;
|
||||
try {
|
||||
process = await _processStart('bluetoothctl', <String>[]);
|
||||
} on ProcessException catch (error) {
|
||||
onLog?.call(
|
||||
'bluetoothctl unavailable, skipping BlueZ disconnect: $error',
|
||||
);
|
||||
return;
|
||||
}
|
||||
process.stdin.writeln('disconnect $remoteId');
|
||||
process.stdin.writeln('quit');
|
||||
try {
|
||||
await process.exitCode.timeout(_processExitTimeout);
|
||||
} catch (_) {
|
||||
process.kill();
|
||||
}
|
||||
onLog?.call('Issued bluetoothctl disconnect for $remoteId');
|
||||
}
|
||||
|
||||
Future<bool> isPairedAndTrusted(String remoteId) async {
|
||||
ProcessResult result;
|
||||
try {
|
||||
result = await _processRun('bluetoothctl', <String>['info', remoteId]);
|
||||
} on ProcessException {
|
||||
return false;
|
||||
}
|
||||
if (result.exitCode != 0) {
|
||||
return false;
|
||||
}
|
||||
final output = (result.stdout as String).toLowerCase();
|
||||
return output.contains('paired: yes') && output.contains('trusted: yes');
|
||||
}
|
||||
|
||||
Future<bool> trustDevice(
|
||||
String remoteId, {
|
||||
void Function(String message)? onLog,
|
||||
}) async {
|
||||
onLog?.call('Requesting BlueZ trust for $remoteId');
|
||||
ProcessResult result;
|
||||
try {
|
||||
result = await _processRun('bluetoothctl', <String>['trust', remoteId]);
|
||||
} on ProcessException catch (error) {
|
||||
onLog?.call('bluetoothctl unavailable, cannot trust $remoteId: $error');
|
||||
return false;
|
||||
}
|
||||
if (result.exitCode != 0) {
|
||||
onLog?.call('bluetoothctl trust failed for $remoteId: ${result.stderr}');
|
||||
return false;
|
||||
}
|
||||
final trusted = await isPairedAndTrusted(remoteId);
|
||||
onLog?.call(
|
||||
trusted
|
||||
? 'Verified BlueZ trust for $remoteId'
|
||||
: 'BlueZ trust verification failed for $remoteId',
|
||||
);
|
||||
return trusted;
|
||||
}
|
||||
|
||||
Future<bool> pairAndTrust({
|
||||
required String remoteId,
|
||||
Duration timeout = _defaultPairingTimeout,
|
||||
void Function(String message)? onLog,
|
||||
Future<String?> Function()? onRequestPin,
|
||||
}) async {
|
||||
var removeRetryUsed = false;
|
||||
var proactivePinRetryUsed = false;
|
||||
Future<String?> Function()? currentPinProvider = onRequestPin;
|
||||
|
||||
for (var attempt = 0; attempt < _maxAttempts; attempt++) {
|
||||
final result = await _runPairingAttempt(
|
||||
remoteId: remoteId,
|
||||
timeout: timeout,
|
||||
onLog: onLog,
|
||||
onRequestPin: currentPinProvider,
|
||||
);
|
||||
|
||||
if (result.success) return true;
|
||||
if (result.userCancelled) {
|
||||
onLog?.call('Pairing cancelled by user; skipping retry/remove flow');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.pairFailed) {
|
||||
if (!removeRetryUsed) {
|
||||
removeRetryUsed = true;
|
||||
onLog?.call(
|
||||
'Pairing failed; removing cached bond and retrying '
|
||||
'(attempt ${attempt + 1}/$_maxAttempts)',
|
||||
);
|
||||
await _removeDevice(remoteId, onLog: onLog);
|
||||
continue;
|
||||
}
|
||||
if (!result.pinSent &&
|
||||
!proactivePinRetryUsed &&
|
||||
currentPinProvider != null) {
|
||||
proactivePinRetryUsed = true;
|
||||
onLog?.call(
|
||||
'Pairing failed before PIN challenge; requesting PIN for '
|
||||
'proactive retry (attempt ${attempt + 1}/$_maxAttempts)',
|
||||
);
|
||||
final pin = await currentPinProvider();
|
||||
if (pin == null) {
|
||||
onLog?.call('PIN entry cancelled for proactive retry');
|
||||
return false;
|
||||
}
|
||||
final capturedPin = pin.trim();
|
||||
currentPinProvider = () async => capturedPin;
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Timeout path — pairing neither succeeded nor failed.
|
||||
onLog?.call('Pairing did not complete before timeout');
|
||||
if (!result.pinSent &&
|
||||
!proactivePinRetryUsed &&
|
||||
currentPinProvider != null) {
|
||||
proactivePinRetryUsed = true;
|
||||
onLog?.call(
|
||||
'No PIN challenge observed before timeout; requesting PIN for '
|
||||
'proactive retry (attempt ${attempt + 1}/$_maxAttempts)',
|
||||
);
|
||||
final pin = await currentPinProvider();
|
||||
if (pin == null) {
|
||||
onLog?.call('PIN entry cancelled for proactive retry after timeout');
|
||||
return false;
|
||||
}
|
||||
final capturedPin = pin.trim();
|
||||
currentPinProvider = () async => capturedPin;
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Runs a single bluetoothctl pairing attempt.
|
||||
///
|
||||
/// Uses a [Completer] to wake as soon as pairing succeeds or fails,
|
||||
/// instead of polling.
|
||||
Future<_PairingResult> _runPairingAttempt({
|
||||
required String remoteId,
|
||||
required Duration timeout,
|
||||
void Function(String message)? onLog,
|
||||
Future<String?> Function()? onRequestPin,
|
||||
}) async {
|
||||
onLog?.call('Starting bluetoothctl pairing flow for $remoteId');
|
||||
Process process;
|
||||
try {
|
||||
process = await _processStart('bluetoothctl', <String>[]);
|
||||
} on ProcessException catch (error) {
|
||||
onLog?.call('bluetoothctl unavailable, cannot run pairing flow: $error');
|
||||
return const _PairingResult();
|
||||
}
|
||||
final output = StringBuffer();
|
||||
var pinSent = false;
|
||||
var sessionClosed = false;
|
||||
var userCancelledPinEntry = false;
|
||||
var confirmationHandled = false;
|
||||
var successHandled = false;
|
||||
var failureHandled = false;
|
||||
var detectorBuffer = '';
|
||||
final pairingDone = Completer<void>();
|
||||
var pairSucceeded = false;
|
||||
var pairFailed = false;
|
||||
|
||||
void writeCmd(String cmd) {
|
||||
if (sessionClosed) return;
|
||||
try {
|
||||
process.stdin.writeln(cmd);
|
||||
} on StateError {
|
||||
sessionClosed = true;
|
||||
onLog?.call('bluetoothctl stdin already closed; ignoring "$cmd"');
|
||||
}
|
||||
}
|
||||
|
||||
unawaited(
|
||||
process.exitCode.then((_) {
|
||||
sessionClosed = true;
|
||||
if (!pairingDone.isCompleted) pairingDone.complete();
|
||||
}),
|
||||
);
|
||||
|
||||
void handleChunk(String chunk) {
|
||||
output.write(chunk);
|
||||
detectorBuffer += chunk.toLowerCase();
|
||||
if (detectorBuffer.length > 4096) {
|
||||
detectorBuffer = detectorBuffer.substring(detectorBuffer.length - 4096);
|
||||
}
|
||||
final lower = detectorBuffer;
|
||||
|
||||
if (!pinSent &&
|
||||
!sessionClosed &&
|
||||
(lower.contains('enter pin code') ||
|
||||
lower.contains('requestpin') ||
|
||||
lower.contains('input pin code') ||
|
||||
lower.contains('request passkey') ||
|
||||
lower.contains('requestpasskey') ||
|
||||
lower.contains('enter passkey'))) {
|
||||
pinSent = true;
|
||||
if (onRequestPin == null) {
|
||||
onLog?.call(
|
||||
'PIN/passkey requested but no onRequestPin callback; '
|
||||
'sending empty line to accept default pairing',
|
||||
);
|
||||
writeCmd('');
|
||||
} else {
|
||||
onLog?.call('Pairing agent is ready for PIN/passkey input');
|
||||
unawaited(
|
||||
Future<void>(() async {
|
||||
String? pin;
|
||||
try {
|
||||
pin = await onRequestPin();
|
||||
} catch (e) {
|
||||
onLog?.call('onRequestPin callback threw: $e');
|
||||
pairFailed = true;
|
||||
writeCmd('cancel');
|
||||
if (!pairingDone.isCompleted) pairingDone.complete();
|
||||
return;
|
||||
}
|
||||
if (pin == null) {
|
||||
if (sessionClosed) {
|
||||
onLog?.call(
|
||||
'PIN prompt resolved after pairing session closed',
|
||||
);
|
||||
return;
|
||||
}
|
||||
onLog?.call('PIN entry cancelled by user; cancelling pairing');
|
||||
userCancelledPinEntry = true;
|
||||
pairFailed = true;
|
||||
writeCmd('cancel');
|
||||
if (!pairingDone.isCompleted) pairingDone.complete();
|
||||
return;
|
||||
}
|
||||
if (sessionClosed) {
|
||||
onLog?.call(
|
||||
'PIN provided after pairing session closed; ignoring',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (pin.trim().isEmpty) {
|
||||
onLog?.call(
|
||||
'Blank PIN submitted; sending empty line to accept default pairing',
|
||||
);
|
||||
writeCmd('');
|
||||
} else {
|
||||
onLog?.call('Submitting PIN/passkey to pairing agent');
|
||||
writeCmd(pin.trim());
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!confirmationHandled &&
|
||||
(lower.contains('confirm passkey') ||
|
||||
lower.contains('requestconfirmation') ||
|
||||
lower.contains('[agent] confirm'))) {
|
||||
confirmationHandled = true;
|
||||
onLog?.call(
|
||||
'Pairing agent requested passkey confirmation; answering yes',
|
||||
);
|
||||
writeCmd('yes');
|
||||
}
|
||||
|
||||
if (!successHandled &&
|
||||
(lower.contains('pairing successful') ||
|
||||
lower.contains('already paired'))) {
|
||||
successHandled = true;
|
||||
onLog?.call('Pairing reported success');
|
||||
pairSucceeded = true;
|
||||
if (!pairingDone.isCompleted) pairingDone.complete();
|
||||
}
|
||||
|
||||
if (!failureHandled &&
|
||||
(lower.contains('failed to pair') ||
|
||||
lower.contains('authenticationfailed') ||
|
||||
lower.contains('authentication failed'))) {
|
||||
failureHandled = true;
|
||||
onLog?.call('Pairing reported authentication failure');
|
||||
pairFailed = true;
|
||||
if (!pairingDone.isCompleted) pairingDone.complete();
|
||||
}
|
||||
}
|
||||
|
||||
final stdoutSub = process.stdout
|
||||
.transform(utf8.decoder)
|
||||
.listen(handleChunk);
|
||||
final stderrSub = process.stderr
|
||||
.transform(utf8.decoder)
|
||||
.listen(handleChunk);
|
||||
|
||||
writeCmd('power on');
|
||||
writeCmd('agent KeyboardDisplay');
|
||||
writeCmd('default-agent');
|
||||
onLog?.call('Waiting for pairing challenge from bluetoothctl agent');
|
||||
writeCmd('pair $remoteId');
|
||||
|
||||
// Wait for the Completer to fire (success/failure/process exit) or timeout.
|
||||
await pairingDone.future.timeout(timeout, onTimeout: () {});
|
||||
|
||||
if (!pairFailed && pairSucceeded) {
|
||||
onLog?.call('Pair succeeded; trusting and connecting device');
|
||||
writeCmd('trust $remoteId');
|
||||
writeCmd('connect $remoteId');
|
||||
}
|
||||
writeCmd('quit');
|
||||
sessionClosed = true;
|
||||
|
||||
try {
|
||||
await process.exitCode.timeout(_pairingCleanupTimeout);
|
||||
} catch (_) {
|
||||
process.kill();
|
||||
}
|
||||
await stdoutSub.cancel();
|
||||
await stderrSub.cancel();
|
||||
|
||||
if (pairFailed) {
|
||||
return _PairingResult(
|
||||
pairFailed: true,
|
||||
pinSent: pinSent,
|
||||
userCancelled: userCancelledPinEntry,
|
||||
);
|
||||
}
|
||||
|
||||
final allOutput = output.toString().toLowerCase();
|
||||
final reportedSuccess =
|
||||
pairSucceeded ||
|
||||
allOutput.contains('pairing successful') ||
|
||||
allOutput.contains('already paired');
|
||||
if (reportedSuccess) {
|
||||
final trusted = await trustDevice(remoteId, onLog: onLog);
|
||||
if (!trusted) {
|
||||
onLog?.call('Pairing completed but BlueZ trust was not restored');
|
||||
}
|
||||
return _PairingResult(success: trusted, pinSent: pinSent);
|
||||
}
|
||||
|
||||
return _PairingResult(pinSent: pinSent);
|
||||
}
|
||||
|
||||
Future<void> _removeDevice(
|
||||
String remoteId, {
|
||||
void Function(String message)? onLog,
|
||||
}) async {
|
||||
Process process;
|
||||
try {
|
||||
process = await _processStart('bluetoothctl', <String>[]);
|
||||
} on ProcessException catch (error) {
|
||||
onLog?.call(
|
||||
'bluetoothctl unavailable, skipping remove for $remoteId: $error',
|
||||
);
|
||||
return;
|
||||
}
|
||||
process.stdin.writeln('remove $remoteId');
|
||||
process.stdin.writeln('quit');
|
||||
try {
|
||||
await process.exitCode.timeout(_processExitTimeout);
|
||||
} catch (_) {
|
||||
process.kill();
|
||||
}
|
||||
onLog?.call('Issued bluetoothctl remove for $remoteId');
|
||||
}
|
||||
}
|
||||
|
||||
/// Outcome of a single bluetoothctl pairing attempt.
|
||||
class _PairingResult {
|
||||
final bool success;
|
||||
final bool pairFailed;
|
||||
final bool pinSent;
|
||||
final bool userCancelled;
|
||||
|
||||
const _PairingResult({
|
||||
this.success = false,
|
||||
this.pairFailed = false,
|
||||
this.pinSent = false,
|
||||
this.userCancelled = false,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/// No-op stub for web builds where dart:io is unavailable.
|
||||
///
|
||||
/// The real implementation lives in linux_ble_pairing_service.dart and is
|
||||
/// selected via conditional import in meshcore_connector.dart.
|
||||
class LinuxBlePairingService {
|
||||
LinuxBlePairingService();
|
||||
|
||||
Future<bool> isBluetoothctlAvailable() async => false;
|
||||
|
||||
Future<void> disconnectDevice(
|
||||
String remoteId, {
|
||||
void Function(String message)? onLog,
|
||||
}) async {}
|
||||
|
||||
Future<bool> isPairedAndTrusted(String remoteId) async => false;
|
||||
|
||||
Future<bool> trustDevice(
|
||||
String remoteId, {
|
||||
void Function(String message)? onLog,
|
||||
}) async => false;
|
||||
|
||||
Future<bool> pairAndTrust({
|
||||
required String remoteId,
|
||||
Duration timeout = const Duration(seconds: 45),
|
||||
void Function(String message)? onLog,
|
||||
Future<String?> Function()? onRequestPin,
|
||||
}) async => false;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,12 @@
|
||||
import 'dart:io' show Platform, File;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../helpers/reaction_helper.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../utils/platform_info.dart';
|
||||
|
||||
class NotificationService {
|
||||
static final NotificationService _instance = NotificationService._internal();
|
||||
@@ -63,14 +66,27 @@ class NotificationService {
|
||||
appUserModelId: 'org.meshcore.open.app',
|
||||
guid: 'e7ea8f85-72f5-4f36-91f6-038f740ccf86',
|
||||
);
|
||||
const linuxSettings = LinuxInitializationSettings(
|
||||
defaultActionName: 'Open notification',
|
||||
);
|
||||
|
||||
const initSettings = InitializationSettings(
|
||||
android: androidSettings,
|
||||
iOS: iosSettings,
|
||||
macOS: macSettings,
|
||||
windows: windowsSettings,
|
||||
linux: linuxSettings,
|
||||
);
|
||||
|
||||
// On Linux, the notifications plugin opens a D-Bus session bus
|
||||
// connection whose async subscription can throw an unhandled
|
||||
// SocketException when the bus socket is missing (e.g. running as
|
||||
// root or inside a container without a session bus).
|
||||
if (PlatformInfo.isLinux && !_isDbusSessionAvailable()) {
|
||||
debugPrint('Skipping notification init: D-Bus session bus unavailable');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await _notifications.initialize(
|
||||
settings: initSettings,
|
||||
@@ -82,6 +98,15 @@ class NotificationService {
|
||||
}
|
||||
}
|
||||
|
||||
static bool _isDbusSessionAvailable() {
|
||||
final addr = Platform.environment['DBUS_SESSION_BUS_ADDRESS'];
|
||||
if (addr != null && addr.isNotEmpty) return true;
|
||||
// Fallback: check the default socket for the current user.
|
||||
final uid = Platform.environment['UID'] ?? Platform.environment['EUID'];
|
||||
final path = '/run/user/${uid ?? '1000'}/bus';
|
||||
return File(path).existsSync();
|
||||
}
|
||||
|
||||
Future<bool> _ensureInitialized() async {
|
||||
if (!_isInitialized) {
|
||||
await initialize();
|
||||
@@ -121,6 +146,19 @@ class NotificationService {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Format special message types for human-readable notifications.
|
||||
static String formatNotificationText(String text) {
|
||||
final trimmed = text.trim();
|
||||
final reaction = ReactionHelper.parseReaction(trimmed);
|
||||
if (reaction != null) {
|
||||
return 'Reacted ${reaction.emoji}';
|
||||
}
|
||||
if (RegExp(r'^g:[A-Za-z0-9_-]+$').hasMatch(trimmed)) {
|
||||
return 'Sent a GIF';
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
Future<void> _showMessageNotificationImpl({
|
||||
required String contactName,
|
||||
required String message,
|
||||
@@ -163,7 +201,7 @@ class NotificationService {
|
||||
await _notifications.show(
|
||||
id: contactId?.hashCode ?? 0,
|
||||
title: contactName,
|
||||
body: message,
|
||||
body: formatNotificationText(message),
|
||||
notificationDetails: notificationDetails,
|
||||
payload: 'message:$contactId',
|
||||
);
|
||||
@@ -208,7 +246,9 @@ class NotificationService {
|
||||
|
||||
try {
|
||||
await _notifications.show(
|
||||
id: contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
|
||||
id: contactId != null
|
||||
? 'advert:$contactId'.hashCode
|
||||
: DateTime.now().millisecondsSinceEpoch,
|
||||
title: _l10n.notification_newTypeDiscovered(contactType),
|
||||
body: contactName,
|
||||
notificationDetails: notificationDetails,
|
||||
@@ -257,7 +297,7 @@ class NotificationService {
|
||||
macOS: macDetails,
|
||||
);
|
||||
|
||||
final preview = message.trim();
|
||||
final preview = formatNotificationText(message.trim());
|
||||
final body = preview.isEmpty
|
||||
? _l10n.notification_receivedNewMessage
|
||||
: preview;
|
||||
@@ -307,6 +347,61 @@ class NotificationService {
|
||||
await _notifications.cancel(id: id);
|
||||
}
|
||||
|
||||
/// Cancel the notification for a specific contact and update the app badge.
|
||||
Future<void> clearContactNotification(
|
||||
String contactId,
|
||||
int totalUnreadCount,
|
||||
) async {
|
||||
if (!await _ensureInitialized()) return;
|
||||
await _notifications.cancel(id: contactId.hashCode);
|
||||
await _updateBadge(totalUnreadCount);
|
||||
}
|
||||
|
||||
/// Cancel the notification for a specific channel and update the app badge.
|
||||
Future<void> clearChannelNotification(
|
||||
int channelIndex,
|
||||
int totalUnreadCount,
|
||||
) async {
|
||||
if (!await _ensureInitialized()) return;
|
||||
await _notifications.cancel(id: channelIndex.hashCode);
|
||||
await _updateBadge(totalUnreadCount);
|
||||
}
|
||||
|
||||
/// Cancel advert notifications for the given contact public key hexes.
|
||||
Future<void> clearAdvertNotifications(List<String> contactIds) async {
|
||||
if (!await _ensureInitialized()) return;
|
||||
for (final id in contactIds) {
|
||||
await _notifications.cancel(id: 'advert:$id'.hashCode);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _updateBadge(int count) async {
|
||||
if (PlatformInfo.isIOS || PlatformInfo.isMacOS) {
|
||||
// On Apple platforms, set the badge number directly via a silent update.
|
||||
final darwinDetails = DarwinNotificationDetails(
|
||||
presentAlert: false,
|
||||
presentSound: false,
|
||||
presentBadge: true,
|
||||
badgeNumber: count,
|
||||
);
|
||||
final details = NotificationDetails(
|
||||
iOS: darwinDetails,
|
||||
macOS: darwinDetails,
|
||||
);
|
||||
// Use a fixed ID so each update replaces the previous one.
|
||||
await _notifications.show(
|
||||
id: 'badge_update'.hashCode,
|
||||
title: null,
|
||||
body: null,
|
||||
notificationDetails: details,
|
||||
);
|
||||
// Immediately cancel the silent notification so it doesn't appear in tray.
|
||||
await _notifications.cancel(id: 'badge_update'.hashCode);
|
||||
}
|
||||
// On Android, badge count is derived from active notifications,
|
||||
// so cancelling the specific notification above is sufficient.
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// Public notification methods (rate limiting is enforced automatically)
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
@@ -349,6 +444,7 @@ class NotificationService {
|
||||
|
||||
Future<void> showChannelMessageNotification({
|
||||
required String channelName,
|
||||
required String senderName,
|
||||
required String message,
|
||||
int? channelIndex,
|
||||
int? badgeCount,
|
||||
@@ -359,7 +455,7 @@ class NotificationService {
|
||||
_PendingNotification(
|
||||
type: _NotificationType.channelMessage,
|
||||
title: channelName,
|
||||
body: message,
|
||||
body: '$senderName: $message',
|
||||
id: channelIndex?.toString(),
|
||||
badgeCount: badgeCount,
|
||||
),
|
||||
|
||||
@@ -9,13 +9,17 @@ class PathHistoryService extends ChangeNotifier {
|
||||
final Map<String, ContactPathHistory> _cache = {};
|
||||
final Map<String, int> _autoRotationIndex = {};
|
||||
final Map<String, _FloodStats> _floodStats = {};
|
||||
final Set<String> _pendingLoads = {};
|
||||
final Map<String, List<_DeferredPathRecord>> _deferredRecords = {};
|
||||
|
||||
// LRU cache eviction tracking
|
||||
static const int _maxCachedContacts = 50;
|
||||
final List<String> _cacheAccessOrder = [];
|
||||
|
||||
static const int _maxHistoryEntries = 100;
|
||||
static const int _autoRotationTopCount = 3;
|
||||
|
||||
int _version = 0;
|
||||
int get version => _version;
|
||||
|
||||
PathHistoryService(this._storage);
|
||||
|
||||
@@ -23,17 +27,21 @@ class PathHistoryService extends ChangeNotifier {
|
||||
// Load cached path histories on startup if needed
|
||||
}
|
||||
|
||||
void handlePathUpdated(Contact contact) {
|
||||
if (contact.pathLength < 0) return;
|
||||
|
||||
void handlePathUpdated(Contact contact, {double initialWeight = 1.0}) {
|
||||
if (contact.pathLength < 0 && contact.path.isEmpty) return;
|
||||
final hopCount = contact.pathLength < 0
|
||||
? contact.path.length
|
||||
: contact.pathLength;
|
||||
_addPathRecord(
|
||||
contactPubKeyHex: contact.publicKeyHex,
|
||||
hopCount: contact.pathLength,
|
||||
hopCount: hopCount,
|
||||
tripTimeMs: 0,
|
||||
wasFloodDiscovery: true,
|
||||
pathBytes: contact.path,
|
||||
successCount: 0,
|
||||
failureCount: 0,
|
||||
routeWeight: initialWeight,
|
||||
timestamp: null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,6 +59,44 @@ class PathHistoryService extends ChangeNotifier {
|
||||
pathBytes: selection.pathBytes,
|
||||
successCount: 0,
|
||||
failureCount: 0,
|
||||
timestamp: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// When a flood message is delivered, credit the contact's current device
|
||||
/// path so that the route the ACK traveled back through gets a weight boost.
|
||||
void recordFloodPathAttribution({
|
||||
required String contactPubKeyHex,
|
||||
required List<int> pathBytes,
|
||||
required int hopCount,
|
||||
int? tripTimeMs,
|
||||
double successIncrement = 0.5,
|
||||
double maxWeight = 5.0,
|
||||
}) {
|
||||
if (pathBytes.isEmpty || hopCount < 0) return;
|
||||
|
||||
final existing = _findPathRecord(contactPubKeyHex, pathBytes);
|
||||
final successCount = (existing?.successCount ?? 0) + 1;
|
||||
final failureCount = existing?.failureCount ?? 0;
|
||||
|
||||
final currentWeight = existing?.routeWeight ?? 1.0;
|
||||
final newWeight = (currentWeight + successIncrement).clamp(0.0, maxWeight);
|
||||
|
||||
debugPrint(
|
||||
'Flood path attribution: crediting path [${pathBytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(',')}] '
|
||||
'for $contactPubKeyHex (weight $currentWeight → $newWeight)',
|
||||
);
|
||||
|
||||
_addPathRecord(
|
||||
contactPubKeyHex: contactPubKeyHex,
|
||||
hopCount: hopCount,
|
||||
tripTimeMs: tripTimeMs ?? existing?.tripTimeMs ?? 0,
|
||||
wasFloodDiscovery: true,
|
||||
pathBytes: pathBytes,
|
||||
successCount: successCount,
|
||||
failureCount: failureCount,
|
||||
routeWeight: newWeight,
|
||||
timestamp: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,6 +105,9 @@ class PathHistoryService extends ChangeNotifier {
|
||||
PathSelection selection, {
|
||||
required bool success,
|
||||
int? tripTimeMs,
|
||||
double successIncrement = 0.5,
|
||||
double failureDecrement = 0.5,
|
||||
double maxWeight = 5.0,
|
||||
}) {
|
||||
if (selection.useFlood) {
|
||||
final stats = _floodStats.putIfAbsent(
|
||||
@@ -79,6 +128,18 @@ class PathHistoryService extends ChangeNotifier {
|
||||
final successCount = (existing?.successCount ?? 0) + (success ? 1 : 0);
|
||||
final failureCount = (existing?.failureCount ?? 0) + (success ? 0 : 1);
|
||||
|
||||
final currentWeight = existing?.routeWeight ?? 1.0;
|
||||
double newWeight;
|
||||
if (success) {
|
||||
newWeight = (currentWeight + successIncrement).clamp(0.0, maxWeight);
|
||||
} else {
|
||||
newWeight = currentWeight - failureDecrement;
|
||||
if (newWeight <= 0) {
|
||||
removePathRecord(contactPubKeyHex, selection.pathBytes);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_addPathRecord(
|
||||
contactPubKeyHex: contactPubKeyHex,
|
||||
hopCount: selection.hopCount,
|
||||
@@ -87,37 +148,68 @@ class PathHistoryService extends ChangeNotifier {
|
||||
pathBytes: selection.pathBytes,
|
||||
successCount: successCount,
|
||||
failureCount: failureCount,
|
||||
routeWeight: newWeight,
|
||||
timestamp: success ? DateTime.now() : existing?.timestamp,
|
||||
);
|
||||
}
|
||||
|
||||
PathSelection getNextAutoPathSelection(String contactPubKeyHex) {
|
||||
final ranked = _getRankedPaths(
|
||||
contactPubKeyHex,
|
||||
).take(_autoRotationTopCount).toList();
|
||||
PathSelection selectPathForAttempt(
|
||||
String contactPubKeyHex, {
|
||||
required int attemptIndex,
|
||||
required int maxRetries,
|
||||
List<PathSelection> recentSelections = const [],
|
||||
}) {
|
||||
if (maxRetries <= 0 || attemptIndex >= maxRetries - 1) {
|
||||
return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true);
|
||||
}
|
||||
|
||||
final ranked = _getRankedPaths(contactPubKeyHex);
|
||||
if (ranked.isEmpty) {
|
||||
return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true);
|
||||
}
|
||||
|
||||
_trackAccess(contactPubKeyHex);
|
||||
|
||||
final selections =
|
||||
ranked
|
||||
.map(
|
||||
(path) => PathSelection(
|
||||
pathBytes: path.pathBytes,
|
||||
hopCount: path.hopCount,
|
||||
useFlood: false,
|
||||
),
|
||||
)
|
||||
.toList()
|
||||
..add(
|
||||
const PathSelection(pathBytes: [], hopCount: -1, useFlood: true),
|
||||
);
|
||||
final recentPaths = recentSelections
|
||||
.where((selection) => !selection.useFlood)
|
||||
.map((selection) => selection.pathBytes)
|
||||
.toList();
|
||||
final candidates = recentPaths.isEmpty
|
||||
? ranked
|
||||
: ranked
|
||||
.where(
|
||||
(path) => !recentPaths.any(
|
||||
(recentPath) => _pathsEqual(path.pathBytes, recentPath),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
final selected = candidates.isNotEmpty
|
||||
? (recentPaths.isEmpty
|
||||
? _selectRotatedCandidate(contactPubKeyHex, candidates)
|
||||
: candidates.first)
|
||||
: ranked.first;
|
||||
|
||||
return PathSelection(
|
||||
pathBytes: selected.pathBytes,
|
||||
hopCount: selected.hopCount,
|
||||
useFlood: false,
|
||||
);
|
||||
}
|
||||
|
||||
PathRecord _selectRotatedCandidate(
|
||||
String contactPubKeyHex,
|
||||
List<PathRecord> candidates,
|
||||
) {
|
||||
if (candidates.length <= 1) {
|
||||
_autoRotationIndex[contactPubKeyHex] = 0;
|
||||
return candidates.first;
|
||||
}
|
||||
|
||||
final currentIndex = _autoRotationIndex[contactPubKeyHex] ?? 0;
|
||||
final selection = selections[currentIndex % selections.length];
|
||||
_autoRotationIndex[contactPubKeyHex] = currentIndex + 1;
|
||||
return selection;
|
||||
final selectedIndex = currentIndex % candidates.length;
|
||||
_autoRotationIndex[contactPubKeyHex] =
|
||||
(selectedIndex + 1) % candidates.length;
|
||||
return candidates[selectedIndex];
|
||||
}
|
||||
|
||||
void _addPathRecord({
|
||||
@@ -128,37 +220,68 @@ class PathHistoryService extends ChangeNotifier {
|
||||
required List<int> pathBytes,
|
||||
required int successCount,
|
||||
required int failureCount,
|
||||
double routeWeight = 1.0,
|
||||
DateTime? timestamp,
|
||||
}) {
|
||||
var history = _cache[contactPubKeyHex];
|
||||
|
||||
if (history == null) {
|
||||
// If a load is already in progress, defer this record
|
||||
if (_pendingLoads.contains(contactPubKeyHex)) {
|
||||
_deferredRecords.putIfAbsent(contactPubKeyHex, () => []);
|
||||
_deferredRecords[contactPubKeyHex]!.add(
|
||||
_DeferredPathRecord(
|
||||
hopCount: hopCount,
|
||||
tripTimeMs: tripTimeMs,
|
||||
wasFloodDiscovery: wasFloodDiscovery,
|
||||
pathBytes: pathBytes,
|
||||
successCount: successCount,
|
||||
failureCount: failureCount,
|
||||
routeWeight: routeWeight,
|
||||
timestamp: timestamp,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingLoads.add(contactPubKeyHex);
|
||||
_loadHistoryFromStorage(contactPubKeyHex).then((loaded) {
|
||||
if (loaded != null) {
|
||||
_cache[contactPubKeyHex] = loaded;
|
||||
_addPathRecordInternal(
|
||||
contactPubKeyHex,
|
||||
hopCount,
|
||||
tripTimeMs,
|
||||
wasFloodDiscovery,
|
||||
pathBytes,
|
||||
successCount,
|
||||
failureCount,
|
||||
);
|
||||
} else {
|
||||
_cache[contactPubKeyHex] = ContactPathHistory(
|
||||
contactPubKeyHex: contactPubKeyHex,
|
||||
recentPaths: [],
|
||||
);
|
||||
_addPathRecordInternal(
|
||||
contactPubKeyHex,
|
||||
hopCount,
|
||||
tripTimeMs,
|
||||
wasFloodDiscovery,
|
||||
pathBytes,
|
||||
successCount,
|
||||
failureCount,
|
||||
);
|
||||
_cache[contactPubKeyHex] =
|
||||
loaded ??
|
||||
ContactPathHistory(
|
||||
contactPubKeyHex: contactPubKeyHex,
|
||||
recentPaths: [],
|
||||
);
|
||||
_addPathRecordInternal(
|
||||
contactPubKeyHex,
|
||||
hopCount,
|
||||
tripTimeMs,
|
||||
wasFloodDiscovery,
|
||||
pathBytes,
|
||||
successCount,
|
||||
failureCount,
|
||||
routeWeight,
|
||||
timestamp,
|
||||
);
|
||||
|
||||
// Apply any deferred records
|
||||
final deferred = _deferredRecords.remove(contactPubKeyHex);
|
||||
if (deferred != null) {
|
||||
for (final record in deferred) {
|
||||
_addPathRecordInternal(
|
||||
contactPubKeyHex,
|
||||
record.hopCount,
|
||||
record.tripTimeMs,
|
||||
record.wasFloodDiscovery,
|
||||
record.pathBytes,
|
||||
record.successCount,
|
||||
record.failureCount,
|
||||
record.routeWeight,
|
||||
record.timestamp,
|
||||
);
|
||||
}
|
||||
}
|
||||
_pendingLoads.remove(contactPubKeyHex);
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -171,6 +294,8 @@ class PathHistoryService extends ChangeNotifier {
|
||||
pathBytes,
|
||||
successCount,
|
||||
failureCount,
|
||||
routeWeight,
|
||||
timestamp,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -182,9 +307,12 @@ class PathHistoryService extends ChangeNotifier {
|
||||
List<int> pathBytes,
|
||||
int successCount,
|
||||
int failureCount,
|
||||
double routeWeight,
|
||||
DateTime? timestamp,
|
||||
) {
|
||||
var history = _cache[contactPubKeyHex];
|
||||
if (history == null) return;
|
||||
_version++;
|
||||
|
||||
final existing = _findPathRecord(contactPubKeyHex, pathBytes);
|
||||
if (existing != null) {
|
||||
@@ -194,16 +322,18 @@ class PathHistoryService extends ChangeNotifier {
|
||||
tripTimeMs = existing.tripTimeMs;
|
||||
}
|
||||
wasFloodDiscovery = existing.wasFloodDiscovery || wasFloodDiscovery;
|
||||
timestamp ??= existing.timestamp;
|
||||
}
|
||||
|
||||
final newRecord = PathRecord(
|
||||
hopCount: hopCount,
|
||||
tripTimeMs: tripTimeMs,
|
||||
timestamp: DateTime.now(),
|
||||
timestamp: timestamp,
|
||||
wasFloodDiscovery: wasFloodDiscovery,
|
||||
pathBytes: pathBytes,
|
||||
successCount: successCount,
|
||||
failureCount: failureCount,
|
||||
routeWeight: routeWeight,
|
||||
);
|
||||
|
||||
final updatedPaths = List<PathRecord>.from(history.recentPaths);
|
||||
@@ -241,6 +371,7 @@ class PathHistoryService extends ChangeNotifier {
|
||||
_cache[contactPubKeyHex] = loaded;
|
||||
_trackAccess(contactPubKeyHex);
|
||||
_evictIfNeeded();
|
||||
_version++;
|
||||
notifyListeners();
|
||||
}
|
||||
});
|
||||
@@ -270,12 +401,30 @@ class PathHistoryService extends ChangeNotifier {
|
||||
return history?.mostRecent;
|
||||
}
|
||||
|
||||
({
|
||||
int successCount,
|
||||
int failureCount,
|
||||
int lastTripTimeMs,
|
||||
DateTime? lastUsed,
|
||||
})?
|
||||
getFloodStats(String contactPubKeyHex) {
|
||||
final stats = _floodStats[contactPubKeyHex];
|
||||
if (stats == null) return null;
|
||||
return (
|
||||
successCount: stats.successCount,
|
||||
failureCount: stats.failureCount,
|
||||
lastTripTimeMs: stats.lastTripTimeMs,
|
||||
lastUsed: stats.lastUsed,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> clearPathHistory(String contactPubKeyHex) async {
|
||||
_cache.remove(contactPubKeyHex);
|
||||
_cacheAccessOrder.remove(contactPubKeyHex);
|
||||
_autoRotationIndex.remove(contactPubKeyHex);
|
||||
_floodStats.remove(contactPubKeyHex);
|
||||
await _storage.clearPathHistory(contactPubKeyHex);
|
||||
_version++;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -295,6 +444,7 @@ class PathHistoryService extends ChangeNotifier {
|
||||
);
|
||||
|
||||
await _storage.savePathHistory(contactPubKeyHex, _cache[contactPubKeyHex]!);
|
||||
_version++;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -315,26 +465,81 @@ class PathHistoryService extends ChangeNotifier {
|
||||
|
||||
final ranked = List<PathRecord>.from(history.recentPaths)
|
||||
..removeWhere((p) => p.pathBytes.isEmpty);
|
||||
final fastestTripMs = _getFastestKnownTripMs(ranked);
|
||||
final highestRouteWeight = _getHighestKnownRouteWeight(ranked);
|
||||
|
||||
ranked.sort((a, b) {
|
||||
final aRate =
|
||||
(a.successCount + 1) / (a.successCount + a.failureCount + 2);
|
||||
final bRate =
|
||||
(b.successCount + 1) / (b.successCount + b.failureCount + 2);
|
||||
if (aRate != bRate) return bRate.compareTo(aRate);
|
||||
if (a.successCount != b.successCount) {
|
||||
return b.successCount.compareTo(a.successCount);
|
||||
final scoreCompare =
|
||||
_scorePathRecord(
|
||||
b,
|
||||
fastestTripMs: fastestTripMs,
|
||||
highestRouteWeight: highestRouteWeight,
|
||||
).compareTo(
|
||||
_scorePathRecord(
|
||||
a,
|
||||
fastestTripMs: fastestTripMs,
|
||||
highestRouteWeight: highestRouteWeight,
|
||||
),
|
||||
);
|
||||
if (scoreCompare != 0) {
|
||||
return scoreCompare;
|
||||
}
|
||||
if (a.routeWeight != b.routeWeight) {
|
||||
return b.routeWeight.compareTo(a.routeWeight);
|
||||
}
|
||||
|
||||
final aTrip = a.tripTimeMs == 0 ? 999999 : a.tripTimeMs;
|
||||
final bTrip = b.tripTimeMs == 0 ? 999999 : b.tripTimeMs;
|
||||
if (aTrip != bTrip) return aTrip.compareTo(bTrip);
|
||||
return b.timestamp.compareTo(a.timestamp);
|
||||
final aTime = a.timestamp ?? DateTime.fromMillisecondsSinceEpoch(0);
|
||||
final bTime = b.timestamp ?? DateTime.fromMillisecondsSinceEpoch(0);
|
||||
return bTime.compareTo(aTime);
|
||||
});
|
||||
|
||||
return ranked;
|
||||
}
|
||||
|
||||
int? _getFastestKnownTripMs(List<PathRecord> paths) {
|
||||
final knownTrips = paths
|
||||
.where((path) => path.tripTimeMs > 0)
|
||||
.map((path) => path.tripTimeMs)
|
||||
.toList();
|
||||
if (knownTrips.isEmpty) return null;
|
||||
return knownTrips.reduce((a, b) => a < b ? a : b);
|
||||
}
|
||||
|
||||
double _getHighestKnownRouteWeight(List<PathRecord> paths) {
|
||||
if (paths.isEmpty) return 1.0;
|
||||
final highestWeight = paths
|
||||
.map((path) => path.routeWeight)
|
||||
.reduce((a, b) => a > b ? a : b);
|
||||
return highestWeight <= 0 ? 1.0 : highestWeight;
|
||||
}
|
||||
|
||||
double _scorePathRecord(
|
||||
PathRecord path, {
|
||||
required int? fastestTripMs,
|
||||
required double highestRouteWeight,
|
||||
}) {
|
||||
final totalAttempts = path.successCount + path.failureCount;
|
||||
final reliability = (path.successCount + 1) / (totalAttempts + 2);
|
||||
final latency = fastestTripMs == null || path.tripTimeMs <= 0
|
||||
? 0.6
|
||||
: (fastestTripMs / path.tripTimeMs).clamp(0.0, 1.0);
|
||||
final freshness = path.timestamp == null
|
||||
? 0.0
|
||||
: 1.0 /
|
||||
(1.0 +
|
||||
(DateTime.now().difference(path.timestamp!).inMinutes /
|
||||
60.0 /
|
||||
24.0));
|
||||
final routeWeight = (path.routeWeight / highestRouteWeight).clamp(0.0, 1.0);
|
||||
|
||||
return (reliability * 0.45) +
|
||||
(latency * 0.25) +
|
||||
(freshness * 0.1) +
|
||||
(routeWeight * 0.2);
|
||||
}
|
||||
|
||||
bool _pathsEqual(List<int> a, List<int> b) {
|
||||
return listEquals(a, b);
|
||||
}
|
||||
@@ -360,6 +565,38 @@ class PathHistoryService extends ChangeNotifier {
|
||||
_floodStats.remove(oldest);
|
||||
}
|
||||
}
|
||||
|
||||
void clearAllHistories() {
|
||||
_cache.clear();
|
||||
_cacheAccessOrder.clear();
|
||||
_autoRotationIndex.clear();
|
||||
_floodStats.clear();
|
||||
_storage.clearAllPathHistories();
|
||||
_version = 0;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class _DeferredPathRecord {
|
||||
final int hopCount;
|
||||
final int tripTimeMs;
|
||||
final bool wasFloodDiscovery;
|
||||
final List<int> pathBytes;
|
||||
final int successCount;
|
||||
final int failureCount;
|
||||
final double routeWeight;
|
||||
final DateTime? timestamp;
|
||||
|
||||
_DeferredPathRecord({
|
||||
required this.hopCount,
|
||||
required this.tripTimeMs,
|
||||
required this.wasFloodDiscovery,
|
||||
required this.pathBytes,
|
||||
required this.successCount,
|
||||
required this.failureCount,
|
||||
this.routeWeight = 1.0,
|
||||
this.timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
class _FloodStats {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import '../models/delivery_observation.dart';
|
||||
import '../models/path_history.dart';
|
||||
import '../storage/prefs_manager.dart';
|
||||
|
||||
@@ -6,6 +7,41 @@ class StorageService {
|
||||
static const String _pathHistoryPrefix = 'path_history_';
|
||||
static const String _pendingMessagesKey = 'pending_messages';
|
||||
static const String _repeaterPasswordsKey = 'repeater_passwords';
|
||||
static const String _repeaterAutoClockSyncAfterLoginKey =
|
||||
'repeater_auto_clock_sync_after_login';
|
||||
static const String _deliveryObservationsKey = 'delivery_observations';
|
||||
|
||||
Future<Map<String, bool>> _loadRepeaterAutoClockSyncAfterLogin() async {
|
||||
final prefs = PrefsManager.instance;
|
||||
final jsonStr = prefs.getString(_repeaterAutoClockSyncAfterLoginKey);
|
||||
|
||||
if (jsonStr == null) return {};
|
||||
|
||||
try {
|
||||
final json = jsonDecode(jsonStr) as Map<String, dynamic>;
|
||||
return json.map((key, value) => MapEntry(key, value == true));
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> getRepeaterAutoClockSyncAfterLoginEnabled(
|
||||
String repeaterPubKeyHex,
|
||||
) async {
|
||||
final settings = await _loadRepeaterAutoClockSyncAfterLogin();
|
||||
return settings[repeaterPubKeyHex] ?? false;
|
||||
}
|
||||
|
||||
Future<void> setRepeaterAutoClockSyncAfterLoginEnabled(
|
||||
String repeaterPubKeyHex,
|
||||
bool enabled,
|
||||
) async {
|
||||
final prefs = PrefsManager.instance;
|
||||
final settings = await _loadRepeaterAutoClockSyncAfterLogin();
|
||||
settings[repeaterPubKeyHex] = enabled;
|
||||
final jsonStr = jsonEncode(settings);
|
||||
await prefs.setString(_repeaterAutoClockSyncAfterLoginKey, jsonStr);
|
||||
}
|
||||
|
||||
Future<void> savePathHistory(
|
||||
String contactPubKeyHex,
|
||||
@@ -122,4 +158,33 @@ class StorageService {
|
||||
final prefs = PrefsManager.instance;
|
||||
await prefs.remove(_repeaterPasswordsKey);
|
||||
}
|
||||
|
||||
Future<void> saveDeliveryObservations(
|
||||
List<DeliveryObservation> observations,
|
||||
) async {
|
||||
final prefs = PrefsManager.instance;
|
||||
final jsonStr = jsonEncode(observations.map((o) => o.toJson()).toList());
|
||||
await prefs.setString(_deliveryObservationsKey, jsonStr);
|
||||
}
|
||||
|
||||
Future<List<DeliveryObservation>> loadDeliveryObservations() async {
|
||||
final prefs = PrefsManager.instance;
|
||||
final jsonStr = prefs.getString(_deliveryObservationsKey);
|
||||
|
||||
if (jsonStr == null) return [];
|
||||
|
||||
try {
|
||||
final list = jsonDecode(jsonStr) as List;
|
||||
return list
|
||||
.map((e) => DeliveryObservation.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearDeliveryObservations() async {
|
||||
final prefs = PrefsManager.instance;
|
||||
await prefs.remove(_deliveryObservationsKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export 'tcp_transport_service_native.dart'
|
||||
if (dart.library.js_interop) 'tcp_transport_service_web.dart';
|
||||
@@ -0,0 +1,210 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'app_debug_log_service.dart';
|
||||
import 'usb_serial_frame_codec.dart';
|
||||
|
||||
class TcpTransportService {
|
||||
final StreamController<Uint8List> _frameController =
|
||||
StreamController<Uint8List>.broadcast();
|
||||
final UsbSerialFrameDecoder _frameDecoder = UsbSerialFrameDecoder();
|
||||
|
||||
StreamSubscription<Uint8List>? _socketSubscription;
|
||||
Socket? _socket;
|
||||
AppDebugLogService? _debugLogService;
|
||||
TcpTransportStatus _status = TcpTransportStatus.disconnected;
|
||||
String? _activeHost;
|
||||
int? _activePort;
|
||||
Future<void> _pendingWrite = Future<void>.value();
|
||||
int _connectGeneration = 0;
|
||||
|
||||
TcpTransportStatus get status => _status;
|
||||
Stream<Uint8List> get frameStream => _frameController.stream;
|
||||
bool get isConnected => _status == TcpTransportStatus.connected;
|
||||
String? get activeEndpoint => _activeHost == null || _activePort == null
|
||||
? null
|
||||
: '$_activeHost:$_activePort';
|
||||
|
||||
void setDebugLogService(AppDebugLogService? service) {
|
||||
_debugLogService = service;
|
||||
}
|
||||
|
||||
Future<void> connect({
|
||||
required String host,
|
||||
required int port,
|
||||
Duration timeout = const Duration(seconds: 10),
|
||||
}) async {
|
||||
if (_status == TcpTransportStatus.connected ||
|
||||
_status == TcpTransportStatus.connecting) {
|
||||
throw StateError('TCP transport is already active');
|
||||
}
|
||||
final trimmedHost = host.trim();
|
||||
if (trimmedHost.isEmpty) {
|
||||
throw ArgumentError.value(host, 'host', 'Host cannot be empty');
|
||||
}
|
||||
if (port < 1 || port > 65535) {
|
||||
throw ArgumentError.value(port, 'port', 'Port must be in 1..65535');
|
||||
}
|
||||
|
||||
_status = TcpTransportStatus.connecting;
|
||||
final generation = ++_connectGeneration;
|
||||
_frameDecoder.reset();
|
||||
|
||||
try {
|
||||
final socket = await Socket.connect(trimmedHost, port, timeout: timeout);
|
||||
if (generation != _connectGeneration ||
|
||||
_status != TcpTransportStatus.connecting) {
|
||||
try {
|
||||
await socket.close();
|
||||
} catch (_) {}
|
||||
try {
|
||||
socket.destroy();
|
||||
} catch (_) {}
|
||||
return;
|
||||
}
|
||||
socket.setOption(SocketOption.tcpNoDelay, true);
|
||||
_socket = socket;
|
||||
_activeHost = trimmedHost;
|
||||
_activePort = port;
|
||||
_socketSubscription = socket.listen(
|
||||
_handleSocketData,
|
||||
onError: _handleSocketError,
|
||||
onDone: _handleSocketDone,
|
||||
);
|
||||
_status = TcpTransportStatus.connected;
|
||||
_debugLogService?.info(
|
||||
'TCP transport opened endpoint=$activeEndpoint',
|
||||
tag: 'TCP',
|
||||
);
|
||||
} catch (error) {
|
||||
await _cleanupFailedConnect();
|
||||
_status = TcpTransportStatus.disconnected;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> write(Uint8List data) async {
|
||||
if (!isConnected || _socket == null) {
|
||||
throw StateError('TCP transport is not connected');
|
||||
}
|
||||
|
||||
final packet = wrapUsbSerialTxFrame(data);
|
||||
_logFrameSummary('TCP TX frame', data);
|
||||
|
||||
final writeTask = _pendingWrite.then((_) async {
|
||||
final socket = _socket;
|
||||
if (!isConnected || socket == null) {
|
||||
throw StateError('TCP transport is not connected');
|
||||
}
|
||||
socket.add(packet);
|
||||
await socket.flush();
|
||||
});
|
||||
|
||||
_pendingWrite = writeTask.catchError((_) {});
|
||||
await writeTask;
|
||||
}
|
||||
|
||||
Future<void> disconnect() async {
|
||||
_connectGeneration += 1;
|
||||
if (_status == TcpTransportStatus.disconnected) return;
|
||||
|
||||
final endpoint = activeEndpoint;
|
||||
_status = TcpTransportStatus.disconnecting;
|
||||
_frameDecoder.reset();
|
||||
_activeHost = null;
|
||||
_activePort = null;
|
||||
|
||||
final subscription = _socketSubscription;
|
||||
_socketSubscription = null;
|
||||
await subscription?.cancel();
|
||||
|
||||
final socket = _socket;
|
||||
_socket = null;
|
||||
try {
|
||||
await socket?.close();
|
||||
} catch (_) {}
|
||||
try {
|
||||
socket?.destroy();
|
||||
} catch (_) {}
|
||||
|
||||
_status = TcpTransportStatus.disconnected;
|
||||
_debugLogService?.info(
|
||||
'TCP transport closed endpoint=${endpoint ?? 'unknown'}',
|
||||
tag: 'TCP',
|
||||
);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
unawaited(disconnect().whenComplete(_closeFrameController));
|
||||
}
|
||||
|
||||
Future<void> _cleanupFailedConnect() async {
|
||||
final subscription = _socketSubscription;
|
||||
_socketSubscription = null;
|
||||
await subscription?.cancel();
|
||||
final socket = _socket;
|
||||
_socket = null;
|
||||
try {
|
||||
await socket?.close();
|
||||
} catch (_) {}
|
||||
try {
|
||||
socket?.destroy();
|
||||
} catch (_) {}
|
||||
_activeHost = null;
|
||||
_activePort = null;
|
||||
_frameDecoder.reset();
|
||||
}
|
||||
|
||||
void _handleSocketData(Uint8List bytes) {
|
||||
for (final packet in _frameDecoder.ingest(bytes)) {
|
||||
if (!packet.isRxFrame) {
|
||||
_debugLogService?.info(
|
||||
'TCP ignored packet start=0x${packet.frameStart.toRadixString(16).padLeft(2, '0')} len=${packet.payload.length}',
|
||||
tag: 'TCP',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
_addFrame(packet.payload);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSocketError(Object error, [StackTrace? stackTrace]) {
|
||||
_addFrameError(error, stackTrace);
|
||||
unawaited(disconnect());
|
||||
}
|
||||
|
||||
void _handleSocketDone() {
|
||||
if (_status == TcpTransportStatus.disconnecting ||
|
||||
_status == TcpTransportStatus.disconnected) {
|
||||
return;
|
||||
}
|
||||
_addFrameError(StateError('TCP socket closed by remote endpoint'));
|
||||
unawaited(disconnect());
|
||||
}
|
||||
|
||||
void _addFrame(Uint8List payload) {
|
||||
if (_frameController.isClosed) return;
|
||||
_frameController.add(payload);
|
||||
}
|
||||
|
||||
void _addFrameError(Object error, [StackTrace? stackTrace]) {
|
||||
if (_frameController.isClosed) return;
|
||||
_frameController.addError(error, stackTrace);
|
||||
}
|
||||
|
||||
void _logFrameSummary(String prefix, Uint8List payload) {
|
||||
final code = payload.isNotEmpty ? payload.first : -1;
|
||||
_debugLogService?.info(
|
||||
'$prefix code=$code len=${payload.length}',
|
||||
tag: 'TCP',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _closeFrameController() async {
|
||||
if (_frameController.isClosed) return;
|
||||
await _frameController.close();
|
||||
}
|
||||
}
|
||||
|
||||
enum TcpTransportStatus { disconnected, connecting, connected, disconnecting }
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'app_debug_log_service.dart';
|
||||
|
||||
class TcpTransportService {
|
||||
AppDebugLogService? _debugLogService;
|
||||
|
||||
Stream<Uint8List> get frameStream => const Stream<Uint8List>.empty();
|
||||
bool get isConnected => false;
|
||||
String? get activeEndpoint => null;
|
||||
|
||||
void setDebugLogService(AppDebugLogService? service) {
|
||||
_debugLogService = service;
|
||||
}
|
||||
|
||||
Future<void> connect({
|
||||
required String host,
|
||||
required int port,
|
||||
Duration timeout = const Duration(seconds: 10),
|
||||
}) async {
|
||||
_debugLogService?.warn(
|
||||
'TCP transport requested on web for $host:$port',
|
||||
tag: 'TCP',
|
||||
);
|
||||
throw UnsupportedError('TCP transport is not supported on web.');
|
||||
}
|
||||
|
||||
Future<void> write(Uint8List data) async {
|
||||
throw UnsupportedError('TCP transport is not supported on web.');
|
||||
}
|
||||
|
||||
Future<void> disconnect() async {}
|
||||
|
||||
void dispose() {}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:ml_algo/ml_algo.dart';
|
||||
import 'package:ml_dataframe/ml_dataframe.dart';
|
||||
import '../models/delivery_observation.dart';
|
||||
import 'storage_service.dart';
|
||||
|
||||
class _ContactStats {
|
||||
int count = 0;
|
||||
double _sum = 0;
|
||||
|
||||
void add(double ms) {
|
||||
count++;
|
||||
_sum += ms;
|
||||
}
|
||||
|
||||
double get mean => _sum / count;
|
||||
}
|
||||
|
||||
class TimeoutPredictionService extends ChangeNotifier {
|
||||
final StorageService? _storage;
|
||||
|
||||
static const int minObservations = 10;
|
||||
static const int maxObservations = 100;
|
||||
static const int _retrainInterval = 5;
|
||||
// 1.5x multiplier on raw prediction to account for variance in delivery
|
||||
// times — tight enough to improve on worst-case physics, loose enough
|
||||
// to avoid premature timeouts from model noise.
|
||||
static const double _safetyMargin = 1.5;
|
||||
static const int _minContactObservations = 10;
|
||||
|
||||
List<DeliveryObservation> _observations = [];
|
||||
LinearRegressor? _model;
|
||||
List<String> _activeFeatures = [];
|
||||
int _observationsSinceLastTrain = 0;
|
||||
final Map<String, _ContactStats> _contactStats = {};
|
||||
Timer? _persistTimer;
|
||||
|
||||
TimeoutPredictionService(StorageService storage) : _storage = storage;
|
||||
TimeoutPredictionService.noStorage() : _storage = null;
|
||||
|
||||
int get observationCount => _observations.length;
|
||||
bool get hasModel => _model != null;
|
||||
|
||||
Future<void> initialize() async {
|
||||
_observations = await _storage?.loadDeliveryObservations() ?? [];
|
||||
_rebuildContactStats();
|
||||
|
||||
if (_observations.length >= minObservations) {
|
||||
_trainModel();
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'TimeoutPrediction: initialized with ${_observations.length} observations, '
|
||||
'model=${_model != null ? "ready" : "waiting for data"}',
|
||||
);
|
||||
}
|
||||
|
||||
void recordObservation({
|
||||
required String contactKey,
|
||||
required int pathLength,
|
||||
required int messageBytes,
|
||||
required int tripTimeMs,
|
||||
int secondsSinceLastRx = 0,
|
||||
}) {
|
||||
final observation = DeliveryObservation(
|
||||
contactKey: contactKey,
|
||||
pathLength: pathLength,
|
||||
messageBytes: messageBytes,
|
||||
secondsSinceLastRx: secondsSinceLastRx,
|
||||
isFlood: pathLength < 0,
|
||||
deliveryMs: tripTimeMs,
|
||||
timestamp: DateTime.now(),
|
||||
);
|
||||
|
||||
_observations.add(observation);
|
||||
if (_observations.length > maxObservations) {
|
||||
_observations.removeAt(0);
|
||||
}
|
||||
|
||||
_contactStats.putIfAbsent(contactKey, () => _ContactStats());
|
||||
_contactStats[contactKey]!.add(tripTimeMs.toDouble());
|
||||
|
||||
_observationsSinceLastTrain++;
|
||||
if (_observationsSinceLastTrain >= _retrainInterval &&
|
||||
_observations.length >= minObservations) {
|
||||
_trainModel();
|
||||
}
|
||||
|
||||
_persistTimer?.cancel();
|
||||
_persistTimer = Timer(const Duration(seconds: 2), () {
|
||||
_storage?.saveDeliveryObservations(_observations);
|
||||
});
|
||||
debugPrint(
|
||||
'TimeoutPrediction: recorded ${tripTimeMs}ms for $pathLength hops '
|
||||
'(${_observations.length} total)',
|
||||
);
|
||||
}
|
||||
|
||||
int? predictTimeout({
|
||||
String? contactKey,
|
||||
required int pathLength,
|
||||
required int messageBytes,
|
||||
int secondsSinceLastRx = 0,
|
||||
}) {
|
||||
if (_model == null) return null;
|
||||
|
||||
try {
|
||||
if (_activeFeatures.isEmpty) return null;
|
||||
|
||||
final allFeatures = {
|
||||
'pathLength': pathLength.toDouble(),
|
||||
'messageBytes': messageBytes.toDouble(),
|
||||
'secSinceRx': secondsSinceLastRx.toDouble(),
|
||||
'isFlood': pathLength < 0 ? 1.0 : 0.0,
|
||||
};
|
||||
final row = _activeFeatures.map((f) => allFeatures[f]!).toList();
|
||||
|
||||
final features = DataFrame(
|
||||
[row],
|
||||
headerExists: false,
|
||||
header: _activeFeatures,
|
||||
);
|
||||
|
||||
final prediction = _model!.predict(features);
|
||||
final rawValue = prediction.rows.first.first;
|
||||
var predictedMs = (rawValue is double)
|
||||
? rawValue
|
||||
: (rawValue as num).toDouble();
|
||||
|
||||
debugPrint(
|
||||
'TimeoutPrediction: raw prediction=$predictedMs for '
|
||||
'pathLength=$pathLength, messageBytes=$messageBytes, '
|
||||
'features=$_activeFeatures',
|
||||
);
|
||||
|
||||
// Sanity check: if prediction is negative or zero, fall back
|
||||
if (predictedMs <= 0) return null;
|
||||
|
||||
// Blend with per-contact mean if enough data
|
||||
if (contactKey != null) {
|
||||
final stats = _contactStats[contactKey];
|
||||
if (stats != null && stats.count >= _minContactObservations) {
|
||||
predictedMs = 0.5 * predictedMs + 0.5 * stats.mean;
|
||||
}
|
||||
}
|
||||
|
||||
// Connector clamps this between physics min/max bounds
|
||||
final timeout = (predictedMs * _safetyMargin).ceil();
|
||||
debugPrint(
|
||||
'TimeoutPrediction: ML timeout ${timeout}ms '
|
||||
'(raw: ${predictedMs.round()}ms, contact: $contactKey)',
|
||||
);
|
||||
return timeout;
|
||||
} catch (e) {
|
||||
debugPrint('TimeoutPrediction: prediction failed: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void _trainModel() {
|
||||
try {
|
||||
// Build feature columns, then exclude any with zero variance
|
||||
// (ml_algo's OLS produces all-zero coefficients for singular matrices)
|
||||
final allNames = ['pathLength', 'messageBytes', 'secSinceRx', 'isFlood'];
|
||||
final allExtractors = <double Function(DeliveryObservation)>[
|
||||
(o) => o.pathLength.toDouble(),
|
||||
(o) => o.messageBytes.toDouble(),
|
||||
(o) => o.secondsSinceLastRx.toDouble(),
|
||||
(o) => o.isFlood ? 1.0 : 0.0,
|
||||
];
|
||||
|
||||
_activeFeatures = [];
|
||||
for (var i = 0; i < allNames.length; i++) {
|
||||
final values = _observations.map(allExtractors[i]).toSet();
|
||||
if (values.length > 1) _activeFeatures.add(allNames[i]);
|
||||
}
|
||||
|
||||
if (_activeFeatures.isEmpty) {
|
||||
debugPrint(
|
||||
'TimeoutPrediction: no features with variance, skipping training',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final header = [..._activeFeatures, 'deliveryMs'];
|
||||
final rows = _observations.map((o) {
|
||||
final row = <double>[];
|
||||
for (var i = 0; i < allNames.length; i++) {
|
||||
if (_activeFeatures.contains(allNames[i])) {
|
||||
row.add(allExtractors[i](o));
|
||||
}
|
||||
}
|
||||
row.add(o.deliveryMs.toDouble());
|
||||
return row;
|
||||
});
|
||||
|
||||
final data = DataFrame([header, ...rows], headerExists: true);
|
||||
|
||||
_model = LinearRegressor(data, 'deliveryMs');
|
||||
_observationsSinceLastTrain = 0;
|
||||
|
||||
// Log training summary with sample predictions
|
||||
final avgMs =
|
||||
_observations.map((o) => o.deliveryMs).reduce((a, b) => a + b) /
|
||||
_observations.length;
|
||||
debugPrint(
|
||||
'TimeoutPrediction: trained on ${_observations.length} observations '
|
||||
'(avg: ${avgMs.round()}ms, features: $_activeFeatures)',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('TimeoutPrediction: training failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_persistTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _rebuildContactStats() {
|
||||
_contactStats.clear();
|
||||
for (final obs in _observations) {
|
||||
_contactStats.putIfAbsent(obs.contactKey, () => _ContactStats());
|
||||
_contactStats[obs.contactKey]!.add(obs.deliveryMs.toDouble());
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user