Compare commits

..

3 Commits

Author SHA1 Message Date
Winston Lowe 49665fd563 Enhance location handling and UI updates
- Refactor location permission handling in SparseLocationLogger
- Add updateMyLocation method to SparseLocationLogger
- Introduce new contact handling in MeshCoreConnector
- Update map screen with a new option to refresh location
- Add localization for "Update Location" in app_en.arb
- Adjust payload types in meshcore_protocol.dart
2026-02-13 16:53:10 -08:00
Winston Lowe 1603adf5dd Add snapToGridCenter method to align position coordinates to grid 2026-02-12 14:18:30 -08:00
Winston Lowe cedbe1dd6c Implement sparse location logging feature and update related services 2026-02-12 14:18:30 -08:00
153 changed files with 5327 additions and 29329 deletions
-38
View File
@@ -1,38 +0,0 @@
name: Deploy to Cloudflare Workers
on:
push:
tags:
- '*'
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
# Match local development version which provides Dart 3.11.0
flutter-version: '3.41.2'
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Get dependencies
run: flutter pub get
- name: Build Web
run: bun run build
- name: Deploy to Cloudflare
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy
-4
View File
@@ -30,7 +30,6 @@ migrate_working_dir/
.flutter-plugins-dependencies
.pub-cache/
.pub/
pubspec.lock
/build/
/coverage/
@@ -83,6 +82,3 @@ keystore.properties
# IDE
.vscode/launch.json
.vscode/settings.json
# Cloudflare Wrangler
.wrangler
View File
-1
View File
@@ -1 +0,0 @@
4.0.0
-1
View File
@@ -1 +0,0 @@
6.2.4
-6
View File
@@ -78,7 +78,6 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
-**Android**: Full support (API 21+)
-**iOS**: Full support (iOS 12+)
- 🚧 **Desktop**: Limited support (macOS/Linux/Windows)
- 🚧 **Web**: Under construction (Chrome)
### Dependencies
@@ -231,11 +230,6 @@ If you find MeshCore Open useful and would like to support development, you can
**Solana Address:** `F15YanjZj96YTBtKJYgNa8RLQLCZkx5CEwogPWkqXeoQ`
**Monero Address:** `453TxnpUqjkJtXxzdjMsrgERNkBRXEGamPbpC45ENrvKAk9tH7kZbxWF82Hz66etgDZyXFPEBU2JUEqhLeJyWt9kBvTVy5m`
**Bitcoin Address:** `bc1qh45x28v8dslcg4v4upmqd9g0mvc3lnyffmyzr5`
Your support helps maintain and improve this open-source project!
## Acknowledgments
+4 -4
View File
@@ -19,13 +19,13 @@ android {
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
isCoreLibraryDesugaringEnabled = true
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
@@ -83,5 +83,5 @@ flutter {
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
}
-1
View File
@@ -19,7 +19,6 @@
<!-- Camera permission for QR code scanning -->
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-feature android:name="android.hardware.usb.host" android:required="false"/>
<application
android:label="meshcore_open"
@@ -1,18 +1,5 @@
package com.meshcore.meshcore_open
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
class MainActivity : FlutterActivity() {
private val usbFunctions by lazy { MeshcoreUsbFunctions(this) }
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
usbFunctions.configureFlutterEngine(flutterEngine)
}
override fun onDestroy() {
usbFunctions.dispose()
super.onDestroy()
}
}
class MainActivity : FlutterActivity()
@@ -1,582 +0,0 @@
package com.meshcore.meshcore_open
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.hardware.usb.UsbConstants
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbDeviceConnection
import android.hardware.usb.UsbEndpoint
import android.hardware.usb.UsbInterface
import android.hardware.usb.UsbManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.util.Locale
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class MeshcoreUsbFunctions(
private val activity: FlutterActivity,
) {
private companion object {
const val usbRecipientInterface = 0x01
}
private val usbMethodChannelName = "meshcore_open/android_usb_serial"
private val usbEventChannelName = "meshcore_open/android_usb_serial_events"
private val usbPermissionAction = "com.meshcore.meshcore_open.USB_PERMISSION"
private val usbManager by lazy {
activity.getSystemService(Context.USB_SERVICE) as UsbManager
}
private val mainHandler = Handler(Looper.getMainLooper())
private val usbIoExecutor: ExecutorService = Executors.newSingleThreadExecutor()
@Volatile private var eventSink: EventChannel.EventSink? = null
@Volatile private var usbConnection: UsbDeviceConnection? = null
@Volatile private var usbInEndpoint: UsbEndpoint? = null
@Volatile private var usbOutEndpoint: UsbEndpoint? = null
@Volatile private var controlInterface: UsbInterface? = null
@Volatile private var dataInterface: UsbInterface? = null
private var readThread: Thread? = null
@Volatile private var isReading = false
@Volatile private var connectedDeviceName: String? = null
private var pendingConnectResult: MethodChannel.Result? = null
private var pendingConnectPortName: String? = null
private var pendingConnectBaudRate: Int = 115200
private data class PortConfig(
val controlInterface: UsbInterface?,
val dataInterface: UsbInterface,
val inEndpoint: UsbEndpoint,
val outEndpoint: UsbEndpoint,
)
private val permissionReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
UsbManager.ACTION_USB_DEVICE_DETACHED -> {
handleUsbDetached(intent)
return
}
usbPermissionAction -> Unit
else -> return
}
val result = pendingConnectResult
val portName = pendingConnectPortName
pendingConnectResult = null
pendingConnectPortName = null
if (result == null || portName == null) {
return
}
val device = findUsbDevice(portName)
if (device == null) {
result.error(
"usb_device_missing",
null,
null,
)
return
}
val granted =
intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
if (!granted || !usbManager.hasPermission(device)) {
result.error("usb_permission_denied", null, null)
return
}
openUsbDevice(device, pendingConnectBaudRate, result)
}
}
fun configureFlutterEngine(flutterEngine: FlutterEngine) {
registerUsbPermissionReceiver()
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, usbMethodChannelName)
.setMethodCallHandler { call, result ->
when (call.method) {
"listPorts" -> result.success(listUsbPorts())
"connect" -> handleUsbConnect(call, result)
"write" -> handleUsbWrite(call, result)
"disconnect" -> {
scheduleCloseUsbConnection {
result.success(null)
}
}
else -> result.notImplemented()
}
}
EventChannel(flutterEngine.dartExecutor.binaryMessenger, usbEventChannelName)
.setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
eventSink = events
}
override fun onCancel(arguments: Any?) {
eventSink = null
}
},
)
}
fun dispose() {
closeUsbConnection()
usbIoExecutor.shutdownNow()
try {
activity.unregisterReceiver(permissionReceiver)
} catch (_: IllegalArgumentException) {
}
}
private fun registerUsbPermissionReceiver() {
val filter =
IntentFilter().apply {
addAction(usbPermissionAction)
addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
activity.registerReceiver(permissionReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else {
@Suppress("DEPRECATION")
activity.registerReceiver(permissionReceiver, filter)
}
}
private fun listUsbPorts(): List<String> {
return usbManager.deviceList.values.map { device ->
val productName = device.productName ?: "USB Serial Device"
val vendorProduct =
String.format(
Locale.US,
"VID:%04X PID:%04X",
device.vendorId,
device.productId,
)
"${device.deviceName} - $productName - $vendorProduct"
}
}
private fun handleUsbConnect(call: MethodCall, result: MethodChannel.Result) {
val portName = call.argument<String>("portName")
val baudRate = call.argument<Int>("baudRate") ?: 115200
if (portName.isNullOrBlank()) {
result.error("usb_invalid_port", null, null)
return
}
val device = findUsbDevice(portName)
if (device == null) {
result.error("usb_device_missing", null, null)
return
}
if (usbManager.hasPermission(device)) {
openUsbDevice(device, baudRate, result)
return
}
if (pendingConnectResult != null) {
result.error("usb_busy", null, null)
return
}
pendingConnectResult = result
pendingConnectPortName = portName
pendingConnectBaudRate = baudRate
val permissionIntent = PendingIntent.getBroadcast(
activity,
0,
Intent(usbPermissionAction).setPackage(activity.packageName),
pendingIntentFlags(),
)
usbManager.requestPermission(device, permissionIntent)
}
private fun handleUsbWrite(call: MethodCall, result: MethodChannel.Result) {
val data = call.argument<ByteArray>("data")
val connection = usbConnection
val endpoint = usbOutEndpoint
if (data == null) {
result.error("usb_invalid_data", null, null)
return
}
if (connection == null || endpoint == null) {
result.error("usb_not_connected", null, null)
return
}
usbIoExecutor.execute {
try {
writeToDevice(data)
mainHandler.post { result.success(null) }
} catch (error: Exception) {
mainHandler.post {
result.error("usb_write_failed", error.message, null)
}
}
}
}
private fun findUsbDevice(portName: String): UsbDevice? {
val devices = usbManager.deviceList.values
val exactMatch = devices.firstOrNull { it.deviceName == portName }
if (exactMatch != null) {
return exactMatch
}
val normalizedName = portName.substringBefore(" - ").trim()
return devices.firstOrNull { it.deviceName == normalizedName }
}
private fun openUsbDevice(
device: UsbDevice,
baudRate: Int,
result: MethodChannel.Result,
) {
usbIoExecutor.execute {
try {
closeUsbConnection()
val config = resolvePortConfig(device)
if (config == null) {
mainHandler.post {
result.error(
"usb_driver_missing",
null,
null,
)
}
return@execute
}
val connection = usbManager.openDevice(device)
if (connection == null) {
mainHandler.post {
result.error(
"usb_open_failed",
null,
null,
)
}
return@execute
}
if (!connection.claimInterface(config.dataInterface, true)) {
connection.close()
mainHandler.post {
result.error(
"usb_open_failed",
null,
null,
)
}
return@execute
}
if (config.controlInterface != null &&
config.controlInterface.id != config.dataInterface.id &&
!connection.claimInterface(config.controlInterface, true)
) {
connection.releaseInterface(config.dataInterface)
connection.close()
mainHandler.post {
result.error(
"usb_open_failed",
null,
null,
)
}
return@execute
}
usbConnection = connection
usbInEndpoint = config.inEndpoint
usbOutEndpoint = config.outEndpoint
controlInterface = config.controlInterface
dataInterface = config.dataInterface
configureDevice(connection, config, baudRate)
connectedDeviceName = device.deviceName
startReadLoop()
mainHandler.post {
result.success(null)
}
} catch (error: Exception) {
closeUsbConnection()
mainHandler.post {
result.error("usb_connect_failed", error.message, null)
}
}
}
}
private fun resolvePortConfig(device: UsbDevice): PortConfig? {
var preferredDataInterface: UsbInterface? = null
var preferredInEndpoint: UsbEndpoint? = null
var preferredOutEndpoint: UsbEndpoint? = null
var fallbackDataInterface: UsbInterface? = null
var fallbackInEndpoint: UsbEndpoint? = null
var fallbackOutEndpoint: UsbEndpoint? = null
var preferredControlInterface: UsbInterface? = null
for (interfaceIndex in 0 until device.interfaceCount) {
val usbInterface = device.getInterface(interfaceIndex)
var inEndpoint: UsbEndpoint? = null
var outEndpoint: UsbEndpoint? = null
for (endpointIndex in 0 until usbInterface.endpointCount) {
val endpoint = usbInterface.getEndpoint(endpointIndex)
if (endpoint.type != UsbConstants.USB_ENDPOINT_XFER_BULK) {
continue
}
when (endpoint.direction) {
UsbConstants.USB_DIR_IN -> if (inEndpoint == null) inEndpoint = endpoint
UsbConstants.USB_DIR_OUT -> if (outEndpoint == null) outEndpoint = endpoint
}
}
val hasDataPair = inEndpoint != null && outEndpoint != null
when {
usbInterface.interfaceClass == UsbConstants.USB_CLASS_COMM &&
preferredControlInterface == null -> {
preferredControlInterface = usbInterface
}
hasDataPair &&
usbInterface.interfaceClass == UsbConstants.USB_CLASS_CDC_DATA -> {
preferredDataInterface = usbInterface
preferredInEndpoint = inEndpoint
preferredOutEndpoint = outEndpoint
}
hasDataPair && fallbackDataInterface == null -> {
fallbackDataInterface = usbInterface
fallbackInEndpoint = inEndpoint
fallbackOutEndpoint = outEndpoint
}
}
}
val dataInterface = preferredDataInterface ?: fallbackDataInterface ?: return null
val inEndpoint = preferredInEndpoint ?: fallbackInEndpoint ?: return null
val outEndpoint = preferredOutEndpoint ?: fallbackOutEndpoint ?: return null
return PortConfig(preferredControlInterface, dataInterface, inEndpoint, outEndpoint)
}
private fun configureDevice(
connection: UsbDeviceConnection,
config: PortConfig,
baudRate: Int,
) {
val control = config.controlInterface ?: return
val lineCoding =
byteArrayOf(
(baudRate and 0xFF).toByte(),
((baudRate shr 8) and 0xFF).toByte(),
((baudRate shr 16) and 0xFF).toByte(),
((baudRate shr 24) and 0xFF).toByte(),
0, // stop bits: 1
0, // parity: none
8, // data bits
)
val lineCodingResult =
connection.controlTransfer(
UsbConstants.USB_DIR_OUT or
UsbConstants.USB_TYPE_CLASS or
usbRecipientInterface,
0x20,
0,
control.id,
lineCoding,
lineCoding.size,
1000,
)
if (lineCodingResult < 0) {
throw IllegalStateException("Failed to configure USB line coding")
}
val controlLineResult =
connection.controlTransfer(
UsbConstants.USB_DIR_OUT or
UsbConstants.USB_TYPE_CLASS or
usbRecipientInterface,
0x22,
0x0001, // DTR on, RTS off
control.id,
null,
0,
1000,
)
if (controlLineResult < 0) {
throw IllegalStateException("Failed to configure USB control line state")
}
}
private fun startReadLoop() {
val connection = usbConnection ?: return
val endpoint = usbInEndpoint ?: return
isReading = true
readThread =
Thread({
val packetSize = endpoint.maxPacketSize.coerceAtLeast(64)
val buffer = ByteArray(packetSize * 4)
try {
while (isReading) {
val bytesRead = connection.bulkTransfer(endpoint, buffer, buffer.size, 250)
if (!isReading) {
break
}
if (bytesRead <= 0) {
continue
}
val packet = buffer.copyOf(bytesRead)
mainHandler.post {
eventSink?.success(packet)
}
}
} catch (error: Exception) {
if (isReading) {
mainHandler.post {
eventSink?.error(
"usb_io_error",
error.message ?: "USB serial I/O error",
null,
)
}
scheduleCloseUsbConnection()
}
}
}, "MeshCoreUsbRead").also { thread ->
thread.isDaemon = true
thread.start()
}
}
private fun writeToDevice(data: ByteArray) {
val connection = usbConnection ?: throw IllegalStateException("USB connection missing")
val endpoint = usbOutEndpoint ?: throw IllegalStateException("USB output endpoint missing")
var offset = 0
val maxPacketSize = endpoint.maxPacketSize.coerceAtLeast(64)
while (offset < data.size) {
val chunkSize = minOf(maxPacketSize, data.size - offset)
val chunk = data.copyOfRange(offset, offset + chunkSize)
val bytesWritten = connection.bulkTransfer(endpoint, chunk, chunkSize, 1000)
if (bytesWritten != chunkSize) {
throw IllegalStateException("Short USB write: wrote $bytesWritten of $chunkSize bytes")
}
offset += chunkSize
}
}
private fun scheduleCloseUsbConnection(onComplete: (() -> Unit)? = null) {
usbIoExecutor.execute {
closeUsbConnection()
if (onComplete != null) {
mainHandler.post(onComplete)
}
}
}
@Synchronized
private fun closeUsbConnection() {
isReading = false
readThread?.interrupt()
if (readThread != null && readThread !== Thread.currentThread()) {
try {
readThread?.join(300)
} catch (_: InterruptedException) {
Thread.currentThread().interrupt()
}
}
readThread = null
val connection = usbConnection
val claimedControl = controlInterface
val claimedData = dataInterface
usbInEndpoint = null
usbOutEndpoint = null
controlInterface = null
dataInterface = null
usbConnection = null
if (connection != null) {
if (claimedControl != null) {
try {
connection.releaseInterface(claimedControl)
} catch (_: Exception) {
}
}
if (claimedData != null && claimedData.id != claimedControl?.id) {
try {
connection.releaseInterface(claimedData)
} catch (_: Exception) {
}
}
try {
connection.close()
} catch (_: Exception) {
}
}
connectedDeviceName = null
}
private fun handleUsbDetached(intent: Intent) {
val detachedDevice =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)
}
val detachedName = detachedDevice?.deviceName ?: return
if (pendingConnectPortName == detachedName) {
pendingConnectResult?.error(
"usb_device_detached",
"USB device was removed before the connection completed",
null,
)
pendingConnectResult = null
pendingConnectPortName = null
}
if (connectedDeviceName == detachedName) {
scheduleCloseUsbConnection {
eventSink?.error(
"usb_device_detached",
"USB device was disconnected",
null,
)
}
}
}
private fun pendingIntentFlags(): Int {
var flags = PendingIntent.FLAG_UPDATE_CURRENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
flags = flags or PendingIntent.FLAG_MUTABLE
}
return flags
}
}
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M268-240 42-466l57-56 170 170 56 56-57 56Zm226 0L268-466l56-57 170 170 368-368 56 57-424 424Zm0-226-57-56 198-198 57 56-198 198Z"/></svg>

Before

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

File diff suppressed because it is too large Load Diff
-70
View File
@@ -1,70 +0,0 @@
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();
}
}
-76
View File
@@ -1,76 +0,0 @@
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);
// --- Label management ---
void updateConnectedLabel(String selfName) {
_service.updateConnectedLabel(selfName);
_activePortLabel = _service.activePortDisplayLabel ?? _activePortLabel;
}
void dispose() {
_service.dispose();
}
}
+48 -164
View File
@@ -4,7 +4,6 @@ import 'dart:typed_data';
// 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);
@@ -14,41 +13,21 @@ 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}',
);
}
final data = _buffer.sublist(_pointer, _pointer + count);
_pointer += count;
return data;
}
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}',
);
}
_pointer += count;
}
Uint8List readRemainingBytes() => readBytes(remaining);
String readString() {
_lastPointer = _pointer;
final value = readRemainingBytes();
try {
return utf8.decode(Uint8List.fromList(value), allowMalformed: true);
} catch (e) {
return String.fromCharCodes(value); // Latin-1 fallback
}
}
String readString() =>
utf8.decode(readRemainingBytes(), allowMalformed: true);
String readCStringGreedy(int maxLength) {
_lastPointer = _pointer;
String readCString(int maxLength) {
final value = <int>[];
final bytes = readBytes(maxLength);
for (final byte in bytes) {
@@ -62,24 +41,6 @@ class BufferReader {
}
}
String readCString(int maxLength) {
final backupPointer = _pointer;
final value = <int>[];
int counter = 0;
while (counter < maxLength) {
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() =>
@@ -101,9 +62,6 @@ class BufferReader {
if ((value & 0x800000) != 0) value -= 0x1000000;
return value;
}
void resetPointer() => _pointer = 0;
void rewind() => _pointer = _lastPointer;
}
// Buffer Writer - accumulating binary data builder
@@ -146,40 +104,25 @@ class BufferWriter {
}
void writeHex(String hex) {
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];
// 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(bytesPadded);
writeBytes(Uint8List.fromList(result));
}
}
Uint8List hex2Uint8List(String hex) {
// Validate hex string length is even and not empty
if (hex.isEmpty || hex.length % 2 != 0) {
throw FormatException('Invalid hex string length: ${hex.length}');
}
List<int> result = [];
for (int i = 0; i < hex.length ~/ 2; i++) {
final hexByte = hex.substring(i * 2, i * 2 + 2);
final byte = int.tryParse(hexByte, radix: 16);
if (byte == null) {
throw FormatException('Invalid hex characters at position $i: $hexByte');
}
result.add(byte);
}
return Uint8List.fromList(result);
}
// Command codes (to device)
const int cmdAppStart = 1;
const int cmdSendTxtMsg = 2;
@@ -209,13 +152,11 @@ const int cmdGetChannel = 31;
const int cmdSetChannel = 32;
const int cmdSendTracePath = 36;
const int cmdSetOtherParams = 38;
const int cmdSendAnonReq = 57;
const int cmdGetRadioSettings = 57;
const int cmdGetTelemetryReq = 39;
const int cmdGetCustomVar = 40;
const int cmdSetCustomVar = 41;
const int cmdSendBinaryReq = 50;
const int cmdSetAutoAddConfig = 58;
const int cmdGetAutoAddConfig = 59;
// Text message types
const int txtTypePlain = 0;
@@ -226,7 +167,7 @@ const int reqTypeGetStatus = 0x01;
const int reqTypeKeepAlive = 0x02;
const int reqTypeGetTelemetry = 0x03;
const int reqTypeGetAccessList = 0x05;
const int reqTypeGetNeighbors = 0x06;
const int reqTypeGetNeighbours = 0x06;
// Repeater response codes
const int respServerLoginOk = 0;
@@ -249,8 +190,8 @@ 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;
// Push codes (async from device)
const int pushCodeAdvert = 0x80;
@@ -296,18 +237,6 @@ 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 maxPathSize = 64;
@@ -351,14 +280,13 @@ int _minPositive(int a, int b) {
const int contactPubKeyOffset = 1;
const int contactTypeOffset = 33;
const int contactFlagsOffset = 34;
const int contactFlagFavorite = 0x01;
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
@@ -647,29 +575,18 @@ Uint8List buildSetChannelFrame(int channelIndex, String name, Uint8List psk) {
}
// Build CMD_SET_RADIO_PARAMS frame
// Format: [cmd][freq x4][bw x4][sf][cr] (pre-v9)
// [cmd][freq x4][bw x4][sf][cr][repeat] (firmware v9+)
// Format: [cmd][freq x4][bw x4][sf][cr]
// freq: frequency in Hz (300000-2500000)
// bw: bandwidth in Hz (7000-500000)
// sf: spreading factor (5-12)
// cr: coding rate (5-8)
// clientRepeat: enable off-grid packet repeat (firmware v9+, omit for older)
Uint8List buildSetRadioParamsFrame(
int freqHz,
int bwHz,
int sf,
int cr, {
bool? clientRepeat,
}) {
Uint8List buildSetRadioParamsFrame(int freqHz, int bwHz, int sf, int cr) {
final writer = BufferWriter();
writer.writeByte(cmdSetRadioParams);
writer.writeUInt32LE(freqHz);
writer.writeUInt32LE(bwHz);
writer.writeByte(sf);
writer.writeByte(cr);
if (clientRepeat != null) {
writer.writeByte(clientRepeat ? 1 : 0);
}
return writer.toBytes();
}
@@ -689,17 +606,14 @@ 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][Lat? x4, Lon? x4][timestamp? x4]
// Format: [cmd][pub_key x32][type][flags][path_len][path x64][name x32][timestamp x4]
Uint8List buildUpdateContactPathFrame(
Uint8List pubKey,
Uint8List path,
Uint8List customPath,
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);
@@ -708,7 +622,17 @@ Uint8List buildUpdateContactPathFrame(
writer.writeByte(flags);
writer.writeByte(pathLen);
writer.writeBytesPadded(path, maxPathSize);
// 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);
// Name (32 bytes, null-padded)
writer.writeCString(name, maxNameSize);
@@ -717,27 +641,6 @@ 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();
}
@@ -750,15 +653,16 @@ 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
@@ -883,10 +787,10 @@ Uint8List buildExportContactFrame(Uint8List pubKey) {
// Build a import contact frame
// [cmd][contact_frame x98+]
Uint8List buildImportContactFrame(Uint8List contactFrame) {
Uint8List buildImportContactFrame(String contactFrame) {
final writer = BufferWriter();
writer.writeByte(cmdImportContact);
writer.writeBytes(contactFrame);
writer.writeHex(contactFrame);
return writer.toBytes();
}
@@ -900,40 +804,20 @@ Uint8List buildZeroHopContact(Uint8List pubKey) {
}
// Build CMD_SET_OTHER_PARAMS frame
// Format: [cmd][allowTelemetryFlags][advertLocationPolicy][multiAcks]
// Format: [cmd][allowAutoAddContacts][allowTelemetryFlags][advertLocationPolicy][multiAcks]
Uint8List buildSetOtherParamsFrame(
bool allowAutoAddContacts,
int allowTelemetryFlags,
int advertLocationPolicy,
int multiAcks,
) {
final writer = BufferWriter();
writer.writeByte(cmdSetOtherParams);
//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(
allowAutoAddContacts ? 0x00 : 0x01,
); // Allow Auto Add Contacts
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();
}
+161 -175
View File
@@ -1,6 +1,4 @@
import 'dart:typed_data';
import 'package:meshcore_open/utils/app_logger.dart';
import '../connector/meshcore_protocol.dart';
class CayenneLpp {
@@ -86,192 +84,180 @@ class CayenneLpp {
static List<Map<String, dynamic>> parse(Uint8List bytes) {
final buffer = BufferReader(bytes);
final telemetry = <Map<String, dynamic>>[];
try {
while (buffer.remaining >= 2) {
final channel = buffer.readUInt8();
final type = buffer.readUInt8();
if (channel == 0 && type == 0) {
break;
}
while (buffer.remaining >= 2) {
final channel = buffer.readUInt8();
final type = buffer.readUInt8();
switch (type) {
case lppGenericSensor:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt32BE(),
});
break;
case lppLuminosity:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt16BE(),
});
break;
case lppPresence:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt8(),
});
break;
case lppTemperature:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readInt16BE() / 10,
});
break;
case lppRelativeHumidity:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt8() / 2,
});
break;
case lppBarometricPressure:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt16BE() / 10,
});
break;
case lppVoltage:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readInt16BE() / 100,
});
break;
case lppCurrent:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readInt16BE() / 1000,
});
break;
case lppPercentage:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt8(),
});
break;
case lppConcentration:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt16BE(),
});
break;
case lppPower:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt16BE(),
});
break;
case lppGps:
telemetry.add({
'channel': channel,
'type': type,
'value': {
'latitude': buffer.readInt24BE() / 10000,
'longitude': buffer.readInt24BE() / 10000,
'altitude': buffer.readInt24BE() / 100,
},
});
break;
default:
return telemetry;
}
if (channel == 0 && type == 0) {
break;
}
switch (type) {
case lppGenericSensor:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt32BE(),
});
break;
case lppLuminosity:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt16BE(),
});
break;
case lppPresence:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt8(),
});
break;
case lppTemperature:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readInt16BE() / 10,
});
break;
case lppRelativeHumidity:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt8() / 2,
});
break;
case lppBarometricPressure:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt16BE() / 10,
});
break;
case lppVoltage:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readInt16BE() / 100,
});
break;
case lppCurrent:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readInt16BE() / 1000,
});
break;
case lppPercentage:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt8(),
});
break;
case lppConcentration:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt16BE(),
});
break;
case lppPower:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt16BE(),
});
break;
case lppGps:
telemetry.add({
'channel': channel,
'type': type,
'value': {
'latitude': buffer.readInt24BE() / 10000,
'longitude': buffer.readInt24BE() / 10000,
'altitude': buffer.readInt24BE() / 100,
},
});
break;
default:
return telemetry;
}
return telemetry;
} catch (e) {
// Handle parsing errors, possibly due to malformed data
appLogger.error('Error parsing Cayenne LPP data: $e');
// Return any telemetry parsed so far to preserve partial data
return telemetry;
}
return telemetry;
}
static List<Map<String, dynamic>> parseByChannel(Uint8List bytes) {
final buffer = BufferReader(bytes);
final Map<int, Map<String, dynamic>> channels = {};
try {
while (buffer.remaining >= 2) {
final channel = buffer.readUInt8();
final type = buffer.readUInt8();
// Optional: stop on padding (00 00)
if (channel == 0 && type == 0) {
break;
}
while (buffer.remaining >= 2) {
final channel = buffer.readUInt8();
final type = buffer.readUInt8();
final channelData = channels.putIfAbsent(
channel,
() => {'channel': channel, 'values': <String, dynamic>{}},
);
switch (type) {
case lppGenericSensor:
channelData['values']['generic'] = buffer.readUInt32BE();
break;
case lppLuminosity:
channelData['values']['luminosity'] = buffer.readUInt16BE();
break;
case lppPresence:
channelData['values']['presence'] = buffer.readUInt8() != 0;
break;
case lppTemperature:
channelData['values']['temperature'] = buffer.readInt16BE() / 10.0;
break;
case lppRelativeHumidity:
channelData['values']['humidity'] = buffer.readUInt8() / 2.0;
break;
case lppBarometricPressure:
channelData['values']['pressure'] = buffer.readUInt16BE() / 10.0;
break;
case lppVoltage:
channelData['values']['voltage'] = buffer.readInt16BE() / 100.0;
break;
case lppCurrent:
channelData['values']['current'] = buffer.readInt16BE() / 1000.0;
break;
case lppPercentage:
channelData['values']['percentage'] = buffer.readUInt8();
break;
case lppConcentration:
channelData['values']['concentration'] = buffer.readUInt16BE();
break;
case lppPower:
channelData['values']['power'] = buffer.readUInt16BE();
break;
case lppGps:
channelData['values']['gps'] = {
'latitude': buffer.readInt24BE() / 10000.0,
'longitude': buffer.readInt24BE() / 10000.0,
'altitude': buffer.readInt24BE() / 100.0,
};
break;
// Add more types as needed...
default:
//Stopped parsing to avoid misalignment
return channels.values.toList();
}
// Optional: stop on padding (00 00)
if (channel == 0 && type == 0) {
break;
}
final List<Map<String, dynamic>> channelsOut = channels.values.toList();
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
return channelsOut;
} catch (e) {
// Handle parsing errors, possibly due to malformed data
appLogger.error('Error parsing Cayenne LPP data: $e');
return <
Map<String, dynamic>
>[]; // Return an empty list on error to avoid crashing the app
final channelData = channels.putIfAbsent(
channel,
() => {'channel': channel, 'values': <String, dynamic>{}},
);
switch (type) {
case lppGenericSensor:
channelData['values']['generic'] = buffer.readUInt32BE();
break;
case lppLuminosity:
channelData['values']['luminosity'] = buffer.readUInt16BE();
break;
case lppPresence:
channelData['values']['presence'] = buffer.readUInt8() != 0;
break;
case lppTemperature:
channelData['values']['temperature'] = buffer.readInt16BE() / 10.0;
break;
case lppRelativeHumidity:
channelData['values']['humidity'] = buffer.readUInt8() / 2.0;
break;
case lppBarometricPressure:
channelData['values']['pressure'] = buffer.readUInt16BE() / 10.0;
break;
case lppVoltage:
channelData['values']['voltage'] = buffer.readInt16BE() / 100.0;
break;
case lppCurrent:
channelData['values']['current'] = buffer.readInt16BE() / 1000.0;
break;
case lppPercentage:
channelData['values']['percentage'] = buffer.readUInt8();
break;
case lppConcentration:
channelData['values']['concentration'] = buffer.readUInt16BE();
break;
case lppPower:
channelData['values']['power'] = buffer.readUInt16BE();
break;
case lppGps:
channelData['values']['gps'] = {
'latitude': buffer.readInt24BE() / 10000.0,
'longitude': buffer.readInt24BE() / 10000.0,
'altitude': buffer.readInt24BE() / 100.0,
};
break;
// Add more types as needed...
default:
// Unknown type: skip or handle error?
continue;
}
}
final List<Map<String, dynamic>> channelsOut = channels.values.toList();
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
return channelsOut;
}
}
-22
View File
@@ -1,22 +0,0 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
class LosIcon extends StatelessWidget {
final double size;
final Color? color;
const LosIcon({super.key, this.size = 24, this.color});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final iconTheme = IconTheme.of(context);
final iconColor =
color ??
iconTheme.color ??
theme.iconTheme.color ??
theme.colorScheme.onSurface;
return Icon(Symbols.elevation, size: size, color: iconColor);
}
}
+11 -307
View File
@@ -1,12 +1,4 @@
{
"channels_channelDeleteFailed": "Неуспешно изтриване на канала \"{name}\"",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "bg",
"appTitle": "MeshCore Open",
"nav_contacts": "Контакти",
@@ -139,6 +131,9 @@
"settings_infoContactsCount": "Брой контакти",
"settings_infoChannelCount": "Брой канали",
"settings_presets": "Предварителни настройки",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
"settings_preset433Mhz": "433 MHz",
"settings_frequency": "Честота (MHz)",
"settings_frequencyHelper": "300.0 - 2500.0",
"settings_frequencyInvalid": "Невалидна честота (300-2500 MHz)",
@@ -148,6 +143,8 @@
"settings_txPower": "TX Мощност (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Невалидна мощност на TX (0-22 dBm)",
"settings_longRange": "Дълъг обхват",
"settings_fastSpeed": "Бърза скорост",
"settings_error": "Грешка: {message}",
"@settings_error": {
"placeholders": {
@@ -342,8 +339,6 @@
"channels_publicChannel": "Публичен канал",
"channels_privateChannel": "Частен канал",
"channels_editChannel": "Редактирай канал",
"channels_muteChannel": "Заглуши канала",
"channels_unmuteChannel": "Включи известията на канала",
"channels_deleteChannel": "Изтрий канала",
"channels_deleteChannelConfirm": "Изтрий \"{name}\"? Това не може да бъде отменено.",
"@channels_deleteChannelConfirm": {
@@ -1361,12 +1356,12 @@
}
}
},
"repeater_neighborsSubtitle": "Преглед на съседни възли с нулев скок.",
"repeater_neighbors": "Съседи",
"repeater_neighboursSubtitle": "Преглед на съседни възли с нулев скок.",
"repeater_neighbours": "Съседи",
"neighbors_receivedData": "Получени данни за съседи",
"neighbors_requestTimedOut": "Съседите поискат изтичане на време.",
"neighbors_errorLoading": "Грешка при зареждане на съседи: {error}",
"neighbors_repeatersNeighbors": "Повторители Съседи",
"neighbors_repeatersNeighbours": "Повторители Съседи",
"neighbors_noData": "Няма налични данни за съседи.",
"channels_createPrivateChannel": "Създай Частен Канал",
"channels_joinPrivateChannel": "Присъедини се към Частен Канал",
@@ -1562,8 +1557,6 @@
"contacts_clipboardEmpty": "Клипборда е празна.",
"contacts_invalidAdvertFormat": "Невалидни данни за контакт",
"appSettings_languageRu": "Руски",
"appSettings_enableMessageTracing": "Разрешаване на проследяване на съобщения",
"appSettings_enableMessageTracingSubtitle": "Показване на подробни метаданни за маршрутизация и синхронизация за съобщения",
"contacts_contactImported": "Контактът е импортиран.",
"contacts_zeroHopAdvert": "Реклама без скок",
"contacts_contactImportFailed": "Контактът не е успешно импортиран.",
@@ -1582,6 +1575,7 @@
"notification_newNodesCount": "{count} {count, plural, =1{нов възел} other{нови възли}}",
"notification_newTypeDiscovered": "Открит нов {contactType}",
"notification_receivedNewMessage": "Получено ново съобщение",
"contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя.",
"settings_gpxExportContactsSubtitle": "Експортира спътници с местоположение в GPX файл.",
"settings_gpxExportRepeatersSubtitle": "Изпраща повторители / roomserver с местоположение в GPX файл.",
"settings_gpxExportAll": "Експортирай всички контакти в GPX",
@@ -1597,296 +1591,6 @@
"settings_gpxExportAllContacts": "Местоположения на всички контакти",
"settings_gpxExportShareText": "Картинни данни изнесени от meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open износ на данни за карта в формат GPX",
"pathTrace_someHopsNoLocation": "Един или повече от хмелите липсва местоположение!",
"map_pathTraceCancelled": "Отменен е следването на пътя.",
"pathTrace_clearTooltip": "Изчисти пътя",
"map_removeLast": "Премахни Последно",
"map_runTrace": "Изпълни Път на Следване",
"map_tapToAdd": "Натиснете върху възлите, за да ги добавите към пътя.",
"scanner_bluetoothOff": "Bluetooth е изключен.",
"scanner_enableBluetooth": "Активирайте Bluetooth",
"scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.",
"scanner_chromeRequired": "Изисква се браузър Chrome",
"scanner_chromeRequiredMessage": "Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.",
"snrIndicator_lastSeen": "Последно видян",
"snrIndicator_nearByRepeaters": "Близки повтарящи се устройства",
"chat_ShowAllPaths": "Покажи всички пътища",
"settings_clientRepeatSubtitle": "Позволете на това устройство да предава пакети към мрежата за други устройства.",
"settings_clientRepeatFreqWarning": "За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.",
"settings_clientRepeat": "Без електричество – повторение",
"settings_aboutOpenMeteoAttribution": "Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "единици",
"appSettings_unitsMetric": "Метрика (m / km)",
"appSettings_unitsImperial": "Имперска (ft / mi)",
"map_lineOfSight": "Линия на видимост",
"map_losScreenTitle": "Линия на видимост",
"losSelectStartEnd": "Изберете начални и крайни възли за LOS.",
"losRunFailed": "Проверката на пряката видимост е неуспешна: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Изчистете всички точки",
"losRunToViewElevationProfile": "Стартирайте LOS, за да видите профила на надморската височина",
"losMenuTitle": "LOS меню",
"losMenuSubtitle": "Докоснете възли или натиснете продължително карта за персонализирани точки",
"losShowDisplayNodes": "Показване на възли на дисплея",
"losCustomPoints": "Персонализирани точки",
"losCustomPointLabel": "Персонализирано {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Точка А",
"losPointB": "Точка Б",
"losAntennaA": "Антена A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Антена B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Стартирайте LOS",
"losNoElevationData": "Няма данни за надморска височина",
"losProfileClear": "{distance} {distanceUnit}, чист LOS, минимално разстояние {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, блокиран от {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: проверка...",
"losStatusNoData": "LOS: няма данни",
"losStatusSummary": "LOS: {clear}/{total} ясно, {blocked} блокирано, {unknown} неизвестно",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Няма налични данни за надморска височина за една или повече проби.",
"losErrorInvalidInput": "Невалидни данни за точки/надморска височина за изчисляване на LOS.",
"losRenameCustomPoint": "Преименувайте персонализирана точка",
"losPointName": "Име на точката",
"losShowPanelTooltip": "Показване на LOS панел",
"losHidePanelTooltip": "Скриване на LOS панела",
"losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Радиохоризонт",
"losLegendLosBeam": "Линия на видимост",
"losLegendTerrain": "Терен",
"losFrequencyLabel": "Честота",
"losFrequencyInfoTooltip": "Преглед на детайли за изчислението",
"losFrequencyDialogTitle": "Изчисляване на радиохоризонта",
"losFrequencyDialogDescription": "Започвайки от k={baselineK} при {baselineFreq} MHz, изчислението коригира k-фактора за текущата {frequencyMHz} MHz лента, която определя границата на извития радиохоризонт.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_removeFromFavorites": "Премахване от списъка с любими",
"listFilter_addToFavorites": "Добави към любими",
"listFilter_favorites": "Любими",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_searchFavorites": "Търсене на {number}{str} любими...",
"contacts_searchRoomServers": "Търсене на {number}{str} сървъри в стаята...",
"contacts_unread": "Непрочетено",
"contacts_searchRepeaters": "Търсене на {number}{str} повтарящи се...",
"contacts_searchContactsNoNumber": "Търси контакти...",
"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": "Задайте като моя местоположение"
"pathTrace_someHopsNoLocation": "Един или повече от хмелите липсва местоположение!"
}
+22 -325
View File
@@ -1,12 +1,4 @@
{
"channels_channelDeleteFailed": "Kanal {name} konnte nicht gelöscht werden",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "de",
"appTitle": "MeshCore Open",
"nav_contacts": "Kontakte",
@@ -139,6 +131,9 @@
"settings_infoContactsCount": "Anzahl Kontakte",
"settings_infoChannelCount": "Anzahl Kanäle",
"settings_presets": "Voreinstellungen",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
"settings_preset433Mhz": "433 MHz",
"settings_frequency": "Frequenz (MHz)",
"settings_frequencyHelper": "300,00 - 2.500,00",
"settings_frequencyInvalid": "Ungültige Frequenz (300-2500 MHz)",
@@ -148,6 +143,8 @@
"settings_txPower": "TX-Leistung (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Ungültige TX-Leistung (0-22 dBm)",
"settings_longRange": "Grosse Reichweite",
"settings_fastSpeed": "Schnelle Geschwindigkeit",
"settings_error": "Fehler: {message}",
"@settings_error": {
"placeholders": {
@@ -296,8 +293,8 @@
"contacts_filterContacts": "Filtert Kontakte...",
"contacts_noContactsMatchFilter": "Keine Kontakte passen zu Ihrem Filter",
"contacts_noMembers": "Keine Mitglieder",
"contacts_lastSeenNow": "kürzlich",
"contacts_lastSeenMinsAgo": "~ {minutes} Min.",
"contacts_lastSeenNow": "gerade gesehen",
"contacts_lastSeenMinsAgo": "Letzte Sichtung vor {minutes} Minuten.",
"@contacts_lastSeenMinsAgo": {
"placeholders": {
"minutes": {
@@ -305,8 +302,8 @@
}
}
},
"contacts_lastSeenHourAgo": "~ 1 Std.",
"contacts_lastSeenHoursAgo": "~ {hours} Std.",
"contacts_lastSeenHourAgo": "Letzte Sichtung vor 1 Stunde.",
"contacts_lastSeenHoursAgo": "Letzte Sichtung vor {hours} Stunden.",
"@contacts_lastSeenHoursAgo": {
"placeholders": {
"hours": {
@@ -314,8 +311,8 @@
}
}
},
"contacts_lastSeenDayAgo": "~ 1 Tag",
"contacts_lastSeenDaysAgo": "~ {days} Tage",
"contacts_lastSeenDayAgo": "Letzte Sichtung vor 1 Tag",
"contacts_lastSeenDaysAgo": "Letzte Sichtung {days} Tage zuvor",
"@contacts_lastSeenDaysAgo": {
"placeholders": {
"days": {
@@ -342,8 +339,6 @@
"channels_publicChannel": "Öffentlicher Kanal",
"channels_privateChannel": "Privater Kanal",
"channels_editChannel": "Kanal bearbeiten",
"channels_muteChannel": "Kanal stummschalten",
"channels_unmuteChannel": "Kanal Stummschaltung aufheben",
"channels_deleteChannel": "Lösche den Kanal",
"channels_deleteChannelConfirm": "Löschen von \"{name}\"? Dies kann nicht rückgängig gemacht werden.",
"@channels_deleteChannelConfirm": {
@@ -1349,9 +1344,6 @@
"listFilter_az": "A-Z",
"listFilter_filters": "Filtere",
"listFilter_all": "Alle",
"listFilter_favorites": "Favoriten",
"listFilter_addToFavorites": "Zu Favoriten hinzufügen",
"listFilter_removeFromFavorites": "Aus Favoriten entfernen",
"listFilter_users": "Benutzer",
"listFilter_repeaters": "Repeater",
"listFilter_roomServers": "Raumserver",
@@ -1364,12 +1356,12 @@
}
}
},
"repeater_neighbors": "Nachbarn",
"repeater_neighborsSubtitle": "Anzahl der Hop-Nachbarn anzeigen.",
"repeater_neighbours": "Nachbarn",
"repeater_neighboursSubtitle": "Anzahl der Hop-Nachbarn anzeigen.",
"neighbors_receivedData": "Empfangene Nachbarsdaten",
"neighbors_requestTimedOut": "Anfrage durch Timeout fehlgeschlagen.",
"neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}",
"neighbors_repeatersNeighbors": "Nachbarn",
"neighbors_repeatersNeighbours": "Nachbarn",
"neighbors_noData": "Keine Nachbarsdaten verfügbar.",
"channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei",
"channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.",
@@ -1565,8 +1557,6 @@
"contacts_invalidAdvertFormat": "Ungültige Kontaktdaten",
"contacts_clipboardEmpty": "Die Zwischenablage ist leer.",
"appSettings_languageUk": "Ukrainisch",
"appSettings_enableMessageTracing": "Nachrichtenverfolgung aktivieren",
"appSettings_enableMessageTracingSubtitle": "Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen",
"contacts_contactImported": "Kontakt wurde importiert.",
"contacts_contactImportFailed": "Kontakt konnte nicht importiert werden",
"contacts_zeroHopAdvert": "Zero-Hop-Ankündigung",
@@ -1579,40 +1569,34 @@
"contacts_zeroHopContactAdvertSent": "Kontakt über Anzeige gesendet",
"contacts_contactAdvertCopied": "Anzeige in die Zwischenablage kopiert.",
"contacts_contactAdvertCopyFailed": "Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.",
"notification_activityTitle": "MeshCore Aktivität",
"notification_messagesCount": "{count} {count, plural, =1{Nachricht} other{Nachrichten}}",
"@notification_messagesCount": {
"placeholders": {
"count": {
"type": "int"
}
"count": {"type": "int"}
}
},
"notification_channelMessagesCount": "{count} {count, plural, =1{Kanalnachricht} other{Kanalnachrichten}}",
"@notification_channelMessagesCount": {
"placeholders": {
"count": {
"type": "int"
}
"count": {"type": "int"}
}
},
"notification_newNodesCount": "{count} {count, plural, =1{neuer Knoten} other{neue Knoten}}",
"@notification_newNodesCount": {
"placeholders": {
"count": {
"type": "int"
}
"count": {"type": "int"}
}
},
"notification_newTypeDiscovered": "Neuer {contactType} entdeckt",
"@notification_newTypeDiscovered": {
"placeholders": {
"contactType": {
"type": "String"
}
"contactType": {"type": "String"}
}
},
"notification_receivedNewMessage": "Neue Nachricht empfangen",
"contacts_contactAdvertCopyFailed": "Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.",
"settings_gpxExportAll": "Alle Knoten als GPX exportieren",
"settings_gpxExportAllSubtitle": "Exportiert alle Knoten mit einem Standort in eine GPX-Datei.",
"settings_gpxExportRepeaters": "Repeater und Raumserver als GPX exportieren",
@@ -1628,293 +1612,6 @@
"settings_gpxExportAllContacts": "Alle Kontaktstandorte",
"settings_gpxExportShareSubject": "GPX-Kartendaten aus meshcore-open exportieren",
"settings_gpxExportShareText": "GPX-Kartendaten aus meshcore-open exportiert",
"pathTrace_someHopsNoLocation": "Bei einer oder mehreren Knoten fehlt der Standort!",
"map_removeLast": "Letztes Entfernen",
"map_tapToAdd": "Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.",
"map_runTrace": "Pfadverlauf ausführen",
"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",
"snrIndicator_nearByRepeaters": "In der Nähe befindliche Repeater",
"chat_ShowAllPaths": "Alle Pfade anzeigen",
"settings_clientRepeat": "Wiederholung, ohne Stromanschluss",
"settings_clientRepeatFreqWarning": "Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.",
"settings_clientRepeatSubtitle": "Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen.",
"settings_aboutOpenMeteoAttribution": "LOS-Höhendaten: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Einheiten",
"appSettings_unitsMetric": "Metrisch (m/km)",
"appSettings_unitsImperial": "Imperial (ft/mi)",
"map_lineOfSight": "Sichtlinie",
"map_losScreenTitle": "Sichtlinie",
"losSelectStartEnd": "Wählen Sie Start- und Endknoten für LOS aus.",
"losRunFailed": "Sichtlinienprüfung fehlgeschlagen: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Löschen Sie alle Punkte",
"losRunToViewElevationProfile": "Führen Sie LOS aus, um das Höhenprofil anzuzeigen",
"losMenuTitle": "LOS-Menü",
"losMenuSubtitle": "Tippen Sie auf Knoten oder drücken Sie lange auf die Karte, um benutzerdefinierte Punkte anzuzeigen",
"losShowDisplayNodes": "Anzeigeknoten anzeigen",
"losCustomPoints": "Benutzerdefinierte Punkte",
"losCustomPointLabel": "Benutzerdefiniert {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Punkt A",
"losPointB": "Punkt B",
"losAntennaA": "Antenne A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antenne B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Führen Sie LOS aus",
"losNoElevationData": "Keine Höhendaten",
"losProfileClear": "{distance} {distanceUnit}, freie Sichtlinie, Mindestabstand {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, blockiert durch {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: Überprüfen...",
"losStatusNoData": "LOS: keine Daten",
"losStatusSummary": "Sichtlinie: {clear}/{total} frei, {blocked} blockiert, {unknown} unbekannt",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Für eine oder mehrere Proben sind keine Höhendaten verfügbar.",
"losErrorInvalidInput": "Ungültige Punkte/Höhendaten für die LOS-Berechnung.",
"losRenameCustomPoint": "Benennen Sie den benutzerdefinierten Punkt um",
"losPointName": "Punktname",
"losShowPanelTooltip": "LOS-Panel anzeigen",
"losHidePanelTooltip": "LOS-Panel ausblenden",
"losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Funkhorizont",
"losLegendLosBeam": "Sichtlinie",
"losLegendTerrain": "Gelände",
"losFrequencyLabel": "Frequenz",
"losFrequencyInfoTooltip": "Details zur Berechnung anzeigen",
"losFrequencyDialogTitle": "Berechnung des Funkhorizonts",
"losFrequencyDialogDescription": "Ausgehend von k={baselineK} bei {baselineFreq} MHz passt die Berechnung den k-Faktor für das aktuelle {frequencyMHz} MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_unread": "Ungelesen",
"contacts_searchContactsNoNumber": "Kontakte suchen...",
"contacts_searchRepeaters": "Suche {number}{str} Repeater...",
"contacts_searchFavorites": "Suche {number}{str} Favoriten...",
"contacts_searchUsers": "Suche {number}{str} Benutzer...",
"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"
"pathTrace_someHopsNoLocation": "Bei einer oder mehreren Knoten fehlt der Standort!"
}
+184 -722
View File
File diff suppressed because it is too large Load Diff
+21 -324
View File
@@ -1,12 +1,4 @@
{
"channels_channelDeleteFailed": "No se pudo eliminar el canal \"{name}\"",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "es",
"appTitle": "MeshCore Open",
"nav_contacts": "Contactos",
@@ -139,6 +131,9 @@
"settings_infoContactsCount": "Número de contactos",
"settings_infoChannelCount": "Número de canales",
"settings_presets": "Preajustes",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
"settings_preset433Mhz": "433 MHz",
"settings_frequency": "Frecuencia (MHz)",
"settings_frequencyHelper": "300,0 - 2500,0",
"settings_frequencyInvalid": "Frecuencia inválida (300-2500 MHz)",
@@ -148,6 +143,8 @@
"settings_txPower": "TX Potencia (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Potencia de TX inválida (0-22 dBm)",
"settings_longRange": "Largo Alcance",
"settings_fastSpeed": "Velocidad Rápida",
"settings_error": "Error: {message}",
"@settings_error": {
"placeholders": {
@@ -297,7 +294,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": "~ {minutes} min.",
"contacts_lastSeenMinsAgo": "Última vez visto hace {minutes} minutos.",
"@contacts_lastSeenMinsAgo": {
"placeholders": {
"minutes": {
@@ -305,8 +302,8 @@
}
}
},
"contacts_lastSeenHourAgo": "~ 1 hora",
"contacts_lastSeenHoursAgo": "~ {hours} horas",
"contacts_lastSeenHourAgo": "Última vez que se vio hace 1 hora",
"contacts_lastSeenHoursAgo": "Última vez visto hace {hours} horas.",
"@contacts_lastSeenHoursAgo": {
"placeholders": {
"hours": {
@@ -314,8 +311,8 @@
}
}
},
"contacts_lastSeenDayAgo": "~ 1 día",
"contacts_lastSeenDaysAgo": "~ {days} días",
"contacts_lastSeenDayAgo": "Última vez que se vio hace 1 día",
"contacts_lastSeenDaysAgo": "Última vez visto hace {days} días.",
"@contacts_lastSeenDaysAgo": {
"placeholders": {
"days": {
@@ -342,8 +339,6 @@
"channels_publicChannel": "Canal público",
"channels_privateChannel": "Canal privado",
"channels_editChannel": "Editar canal",
"channels_muteChannel": "Silenciar canal",
"channels_unmuteChannel": "Activar canal",
"channels_deleteChannel": "Eliminar canal",
"channels_deleteChannelConfirm": "Eliminar \"{name}\"? Esto no se puede deshacer.",
"@channels_deleteChannelConfirm": {
@@ -1361,12 +1356,12 @@
}
}
},
"repeater_neighbors": "Vecinos",
"repeater_neighborsSubtitle": "Ver vecinos de salto cero.",
"repeater_neighbours": "Vecinos",
"repeater_neighboursSubtitle": "Ver vecinos de salto cero.",
"neighbors_receivedData": "Recibidas Datos de Vecinos",
"neighbors_requestTimedOut": "Los vecinos solicitan que se desconecte.",
"neighbors_errorLoading": "Error al cargar vecinos: {error}",
"neighbors_repeatersNeighbors": "Repetidores Vecinos",
"neighbors_repeatersNeighbours": "Repetidores Vecinos",
"neighbors_noData": "No hay datos de vecinos disponibles.",
"channels_joinPrivateChannel": "Únete a un Canal Privado",
"channels_createPrivateChannel": "Crear un Canal Privado",
@@ -1561,8 +1556,6 @@
"appSettings_languageUk": "Ucraniano",
"contacts_clipboardEmpty": "El portapapeles está vacío.",
"appSettings_languageRu": "Ruso",
"appSettings_enableMessageTracing": "Habilitar seguimiento de mensajes",
"appSettings_enableMessageTracingSubtitle": "Mostrar metadatos detallados de enrutamiento y tiempo para los mensajes",
"contacts_invalidAdvertFormat": "Datos de contacto no válidos",
"contacts_floodAdvert": "Anuncio de inundación",
"contacts_contactImported": "El contacto ha sido importado.",
@@ -1576,40 +1569,34 @@
"contacts_zeroHopContactAdvertSent": "Envió contacto por anuncio.",
"contacts_contactAdvertCopied": "Anuncio copiado al Portapapeles.",
"contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado.",
"notification_activityTitle": "Actividad de MeshCore",
"notification_messagesCount": "{count} {count, plural, =1{mensaje} other{mensajes}}",
"@notification_messagesCount": {
"placeholders": {
"count": {
"type": "int"
}
"count": {"type": "int"}
}
},
"notification_channelMessagesCount": "{count} {count, plural, =1{mensaje de canal} other{mensajes de canal}}",
"@notification_channelMessagesCount": {
"placeholders": {
"count": {
"type": "int"
}
"count": {"type": "int"}
}
},
"notification_newNodesCount": "{count} {count, plural, =1{nuevo nodo} other{nuevos nodos}}",
"@notification_newNodesCount": {
"placeholders": {
"count": {
"type": "int"
}
"count": {"type": "int"}
}
},
"notification_newTypeDiscovered": "Nuevo {contactType} descubierto",
"@notification_newTypeDiscovered": {
"placeholders": {
"contactType": {
"type": "String"
}
"contactType": {"type": "String"}
}
},
"notification_receivedNewMessage": "Nuevo mensaje recibido",
"contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado.",
"settings_gpxExportContactsSubtitle": "Exporta compañeros con una ubicación a archivo GPX.",
"settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala a GPX",
"settings_gpxExportSuccess": "Archivo GPX exportado con éxito.",
@@ -1625,296 +1612,6 @@
"settings_gpxExportAllContacts": "Todas las ubicaciones de contactos",
"settings_gpxExportShareText": "Datos del mapa exportados desde meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open exportación de datos de mapa GPX",
"pathTrace_someHopsNoLocation": "Uno o más de los lúpulos carecen de una ubicación",
"pathTrace_clearTooltip": "Borrar ruta",
"map_runTrace": "Ejecutar Rastreo de Ruta",
"map_tapToAdd": "Pulse en los nodos para agregarlos al camino.",
"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",
"snrIndicator_lastSeen": "Visto por última vez",
"chat_ShowAllPaths": "Mostrar todos los caminos",
"settings_clientRepeatFreqWarning": "Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.",
"settings_clientRepeat": "Repetir sin conexión",
"settings_clientRepeatSubtitle": "Permita que este dispositivo repita los paquetes de red para otros usuarios.",
"settings_aboutOpenMeteoAttribution": "Datos de elevación LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Unidades",
"appSettings_unitsMetric": "Métrico (m/km)",
"appSettings_unitsImperial": "Imperial (pies/millas)",
"map_lineOfSight": "Línea de visión",
"map_losScreenTitle": "Línea de visión",
"losSelectStartEnd": "Seleccione los nodos de inicio y fin para LOS.",
"losRunFailed": "Error en la comprobación de la línea de visión: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Borrar todos los puntos",
"losRunToViewElevationProfile": "Ejecute LOS para ver el perfil de elevación",
"losMenuTitle": "Menú LOS",
"losMenuSubtitle": "Toque nodos o mantenga presionado el mapa para puntos personalizados",
"losShowDisplayNodes": "Mostrar nodos de visualización",
"losCustomPoints": "Puntos personalizados",
"losCustomPointLabel": "Personalizado {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Punto A",
"losPointB": "Punto B",
"losAntennaA": "Antena A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antena B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Ejecutar LOS",
"losNoElevationData": "Sin datos de elevación",
"losProfileClear": "{distance} {distanceUnit}, despejar LOS, autorización mínima {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, bloqueado por {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: comprobando...",
"losStatusNoData": "LOS: sin datos",
"losStatusSummary": "LOS: {clear}/{total} claro, {blocked} bloqueado, {unknown} desconocido",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Datos de elevación no disponibles para una o más muestras.",
"losErrorInvalidInput": "Datos de puntos/elevación no válidos para el cálculo de LOS.",
"losRenameCustomPoint": "Cambiar el nombre del punto personalizado",
"losPointName": "Nombre del punto",
"losShowPanelTooltip": "Mostrar panel LOS",
"losHidePanelTooltip": "Ocultar panel LOS",
"losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Horizonte radioeléctrico",
"losLegendLosBeam": "Línea de visión",
"losLegendTerrain": "Terreno",
"losFrequencyLabel": "Frecuencia",
"losFrequencyInfoTooltip": "Ver detalles del cálculo",
"losFrequencyDialogTitle": "Cálculo del horizonte radioeléctrico",
"losFrequencyDialogDescription": "A partir de k={baselineK} en {baselineFreq} MHz, el cálculo ajusta el factor k para la banda actual de {frequencyMHz} MHz, que define el límite curvo del horizonte de radio.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_favorites": "Favoritos",
"listFilter_removeFromFavorites": "Eliminar de las favoritas",
"listFilter_addToFavorites": "Añadir a favoritos",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_searchContactsNoNumber": "Buscar contactos...",
"contacts_unread": "No leído",
"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...",
"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"
"pathTrace_someHopsNoLocation": "Uno o más de los lúpulos carecen de una ubicación"
}
+17 -315
View File
@@ -1,12 +1,4 @@
{
"channels_channelDeleteFailed": "Échec de la suppression de la chaîne \"{name}\"",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "fr",
"appTitle": "MeshCore Open",
"nav_contacts": "Contacts",
@@ -139,6 +131,9 @@
"settings_infoContactsCount": "Nombre de contacts",
"settings_infoChannelCount": "Nombre de canaux",
"settings_presets": "Préréglages",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
"settings_preset433Mhz": "433 MHz",
"settings_frequency": "Fréquence (MHz)",
"settings_frequencyHelper": "300,0 - 2 500,0",
"settings_frequencyInvalid": "Fréquence invalide (300-2500 MHz)",
@@ -148,6 +143,8 @@
"settings_txPower": "TX Puissance (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Puissance TX invalide (0-22 dBm)",
"settings_longRange": "Portée Longue",
"settings_fastSpeed": "Vitesse Rapide",
"settings_error": "Erreur : {message}",
"@settings_error": {
"placeholders": {
@@ -270,7 +267,7 @@
}
},
"contacts_manageRepeater": "Gérer le répéteur",
"contacts_roomLogin": "Connexion Room Server",
"contacts_roomLogin": "Connexion Salle",
"contacts_openChat": "Ouverture du Chat",
"contacts_editGroup": "Modifier le groupe",
"contacts_deleteGroup": "Supprimer le groupe",
@@ -297,7 +294,7 @@
"contacts_noContactsMatchFilter": "Aucun contact ne correspond à votre filtre.",
"contacts_noMembers": "Aucun membre",
"contacts_lastSeenNow": "Vu maintenant",
"contacts_lastSeenMinsAgo": "~ {minutes} min.",
"contacts_lastSeenMinsAgo": "Vu il y a {minutes} minutes",
"@contacts_lastSeenMinsAgo": {
"placeholders": {
"minutes": {
@@ -305,8 +302,8 @@
}
}
},
"contacts_lastSeenHourAgo": "~ 1 heure",
"contacts_lastSeenHoursAgo": "~ {hours} heures",
"contacts_lastSeenHourAgo": "Vu il y a 1 heure",
"contacts_lastSeenHoursAgo": "Vu il y a {hours} heures",
"@contacts_lastSeenHoursAgo": {
"placeholders": {
"hours": {
@@ -314,8 +311,8 @@
}
}
},
"contacts_lastSeenDayAgo": "~ 1 jour",
"contacts_lastSeenDaysAgo": "~ {days} jours",
"contacts_lastSeenDayAgo": "Vu il y a 1 jour",
"contacts_lastSeenDaysAgo": "Vu il y a {days} jours",
"@contacts_lastSeenDaysAgo": {
"placeholders": {
"days": {
@@ -342,8 +339,6 @@
"channels_publicChannel": "Canal public",
"channels_privateChannel": "Canal privé",
"channels_editChannel": "Modifier le canal",
"channels_muteChannel": "Désactiver les notifications du canal",
"channels_unmuteChannel": "Réactiver les notifications du canal",
"channels_deleteChannel": "Supprimer le canal",
"channels_deleteChannelConfirm": "Supprimer {name}? Cela ne peut pas être annulé.",
"@channels_deleteChannelConfirm": {
@@ -806,7 +801,7 @@
"dialog_disconnect": "Déconnecter",
"dialog_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?",
"login_repeaterLogin": "Connexion au répéteur",
"login_roomLogin": "Connexion Room Server",
"login_roomLogin": "Connexion Salle",
"login_password": "Mot de passe",
"login_enterPassword": "Entrez votre mot de passe",
"login_savePassword": "Sauvegarder le mot de passe",
@@ -1361,12 +1356,12 @@
}
}
},
"repeater_neighbors": "Voisins",
"repeater_neighborsSubtitle": "Afficher les voisins de saut nuls.",
"repeater_neighbours": "Voisins",
"repeater_neighboursSubtitle": "Afficher les voisins de saut nuls.",
"neighbors_receivedData": "Données des voisins reçues",
"neighbors_requestTimedOut": "Les voisins demandent un délai.",
"neighbors_errorLoading": "Erreur lors du chargement des voisins : {error}",
"neighbors_repeatersNeighbors": "Répéteurs Voisins",
"neighbors_repeatersNeighbours": "Répéteurs Voisins",
"neighbors_noData": "Aucune donnée concernant les voisins disponible.",
"channels_createPrivateChannelDesc": "Sécurisé avec une clé secrète.",
"channels_joinPrivateChannel": "Rejoindre un Canal Privé",
@@ -1401,7 +1396,7 @@
"settings_locationIntervalSec": "Intervalle de mise-à-jour du GPS (Secondes)",
"settings_locationIntervalInvalid": "L'intervalle doit être compris entre 60 et 86400 secondes.",
"contacts_manageRoom": "Gérer le Room Server",
"room_management": "Administrattion Room Server",
"room_management": "Administración del Servidor de Habitación",
"@community_joinConfirmation": {
"placeholders": {
"name": {
@@ -1561,8 +1556,6 @@
"contacts_invalidAdvertFormat": "Données de contact non valides",
"appSettings_languageUk": "Ukrainien",
"appSettings_languageRu": "Russe",
"appSettings_enableMessageTracing": "Activer le traçage des messages",
"appSettings_enableMessageTracingSubtitle": "Afficher les métadonnées détaillées de routage et de synchronisation des messages",
"contacts_clipboardEmpty": "Le presse-papiers est vide.",
"contacts_contactImported": "Le contact a été importé.",
"contacts_floodAdvert": "Annonce à tout le réseau",
@@ -1597,296 +1590,5 @@
"settings_gpxExportAllContacts": "Tous les emplacements des contacts",
"settings_gpxExportShareText": "Données de carte exportées à partir de meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open exporter les données de carte GPX",
"pathTrace_someHopsNoLocation": "Un ou plusieurs des sauts manquent d'une localisation !",
"map_tapToAdd": "Appuyez sur les nœuds pour les ajouter au chemin.",
"pathTrace_clearTooltip": "Effacer le chemin",
"map_pathTraceCancelled": "Traçage de chemin annulé",
"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",
"snrIndicator_nearByRepeaters": "Répéteurs à proximité",
"chat_ShowAllPaths": "Afficher tous les chemins",
"settings_clientRepeatFreqWarning": "Pour les transmissions hors réseau, il est nécessaire d'utiliser les fréquences de 433, 869 ou 918 MHz.",
"settings_clientRepeatSubtitle": "Permettez à cet appareil de répéter les paquets de données pour les autres.",
"settings_clientRepeat": "Répétition hors réseau",
"settings_aboutOpenMeteoAttribution": "Données d'élévation LOS : Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Unités",
"appSettings_unitsMetric": "Métrique (m/km)",
"appSettings_unitsImperial": "Impérial (ft / mi)",
"map_lineOfSight": "Ligne de vue",
"map_losScreenTitle": "Ligne de vue",
"losSelectStartEnd": "Sélectionnez les nœuds de début et de fin pour LOS.",
"losRunFailed": "Échec de la vérification de la ligne de vue : {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Effacer tous les points",
"losRunToViewElevationProfile": "Exécutez LOS pour afficher le profil d'altitude",
"losMenuTitle": "Menu LOS",
"losMenuSubtitle": "Appuyez sur les nœuds ou appuyez longuement sur la carte pour des points personnalisés",
"losShowDisplayNodes": "Afficher les nœuds d'affichage",
"losCustomPoints": "Points personnalisés",
"losCustomPointLabel": "Personnalisé {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Point A",
"losPointB": "Point B",
"losAntennaA": "Antenne A : {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antenne B : {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Exécuter la LOS",
"losNoElevationData": "Aucune donnée d'altitude",
"losProfileClear": "{distance} {distanceUnit}, LOS clair, clairance minimale {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, bloqué par {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS : vérification...",
"losStatusNoData": "LOS : aucune donnée",
"losStatusSummary": "LOS : {clear}/{total} clair, {blocked} bloqué, {unknown} inconnu",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Données d'altitude indisponibles pour un ou plusieurs échantillons.",
"losErrorInvalidInput": "Données de points/d'altitude non valides pour le calcul de la LOS.",
"losRenameCustomPoint": "Renommer le point personnalisé",
"losPointName": "Nom du point",
"losShowPanelTooltip": "Afficher le panneau LOS",
"losHidePanelTooltip": "Masquer le panneau LOS",
"losElevationAttribution": "Données daltitude : Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Horizon radio",
"losLegendLosBeam": "Ligne de visée",
"losLegendTerrain": "Terrain",
"losFrequencyLabel": "Fréquence",
"losFrequencyInfoTooltip": "Voir les détails du calcul",
"losFrequencyDialogTitle": "Calcul de lhorizon radio",
"losFrequencyDialogDescription": "À partir de k={baselineK} à {baselineFreq} MHz, le calcul ajuste le facteur k pour la bande actuelle de {frequencyMHz} MHz, ce qui définit la limite incurvée de l'horizon radio.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_addToFavorites": "Ajouter à mes favoris",
"listFilter_removeFromFavorites": "Supprimer des favoris",
"listFilter_favorites": "Préférences",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"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_searchRepeaters": "Rechercher {number}{str} Répéteurs...",
"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 serveurs de salle",
"contactsSettings_autoAddRoomServersSubtitle": "Autoriser le compagnon à ajouter automatiquement les serveurs de salles 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"
"pathTrace_someHopsNoLocation": "Un ou plusieurs des sauts manquent d'une localisation !"
}
+11 -307
View File
@@ -1,12 +1,4 @@
{
"channels_channelDeleteFailed": "Impossibile eliminare il canale \"{name}\"",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "it",
"appTitle": "MeshCore Open",
"nav_contacts": "Contatti",
@@ -139,6 +131,9 @@
"settings_infoContactsCount": "Numero contatti",
"settings_infoChannelCount": "Numero Canale",
"settings_presets": "Preset",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
"settings_preset433Mhz": "433 MHz",
"settings_frequency": "Frequenza (MHz)",
"settings_frequencyHelper": "300,0 - 2500,0",
"settings_frequencyInvalid": "Frequenza non valida (300-2500 MHz)",
@@ -148,6 +143,8 @@
"settings_txPower": "TX Potenza (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Potere TX non valido (0-22 dBm)",
"settings_longRange": "Lungo Raggio",
"settings_fastSpeed": "Velocità Rapida",
"settings_error": "Errore: {message}",
"@settings_error": {
"placeholders": {
@@ -342,8 +339,6 @@
"channels_publicChannel": "Canale pubblico",
"channels_privateChannel": "Canale privato",
"channels_editChannel": "Modifica canale",
"channels_muteChannel": "Silenzia canale",
"channels_unmuteChannel": "Attiva notifiche canale",
"channels_deleteChannel": "Elimina canale",
"channels_deleteChannelConfirm": "Eliminare \"{name}\"? Non può essere annullato.",
"@channels_deleteChannelConfirm": {
@@ -1361,12 +1356,12 @@
}
}
},
"repeater_neighbors": "Vicini",
"repeater_neighborsSubtitle": "Visualizza vicini di salto pari a zero.",
"repeater_neighbours": "Vicini",
"repeater_neighboursSubtitle": "Visualizza vicini di salto pari a zero.",
"neighbors_receivedData": "Ricevute dati vicini",
"neighbors_requestTimedOut": "I vicini richiedono un timeout.",
"neighbors_errorLoading": "Errore nel caricamento dei vicini: {error}",
"neighbors_repeatersNeighbors": "Ripetitori Vicini",
"neighbors_repeatersNeighbours": "Ripetitori Vicini",
"neighbors_noData": "Nessun dato sugli vicini disponibile.",
"channels_createPrivateChannel": "Crea un Canale Privato",
"channels_createPrivateChannelDesc": "Protetta con una chiave segreta.",
@@ -1561,8 +1556,6 @@
"appSettings_languageRu": "Russo",
"contacts_invalidAdvertFormat": "Dati di contatto non validi",
"appSettings_languageUk": "Ucraino",
"appSettings_enableMessageTracing": "Abilita tracciamento messaggi",
"appSettings_enableMessageTracingSubtitle": "Mostra metadati dettagliati su instradamento e tempi per i messaggi",
"contacts_zeroHopAdvert": "Annuncio Zero Hop",
"contacts_floodAdvert": "Annuncio alluvionale",
"contacts_copyAdvertToClipboard": "Copia Annuncio negli Appunti",
@@ -1582,6 +1575,7 @@
"notification_newNodesCount": "{count} {count, plural, =1{nuovo nodo} other{nuovi nodi}}",
"notification_newTypeDiscovered": "Nuovo {contactType} scoperto",
"notification_receivedNewMessage": "Nuovo messaggio ricevuto",
"contacts_contactAdvertCopied": "Annuncio copiato negli Appunti.",
"settings_gpxExportRepeaters": "Esporta ripetitori / server di stanza in GPX",
"settings_gpxExportContacts": "Esporta compagni in GPX",
"settings_gpxExportSuccess": "Esportazione del file GPX completata con successo.",
@@ -1597,296 +1591,6 @@
"settings_gpxExportAllContacts": "Tutte le posizioni dei contatti",
"settings_gpxExportShareText": "Dati mappa esportati da meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open esportazione dati mappa GPX",
"pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!",
"map_removeLast": "Rimuovi ultimo",
"map_pathTraceCancelled": "Tracciamento del percorso annullato.",
"pathTrace_clearTooltip": "Pulisci percorso",
"map_runTrace": "Esegui Path Trace",
"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",
"chat_ShowAllPaths": "Mostra tutti i percorsi",
"settings_clientRepeat": "Ripetizione \"fuori dalla rete\"",
"settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.",
"settings_clientRepeatSubtitle": "Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri.",
"settings_aboutOpenMeteoAttribution": "Dati di elevazione LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Unità",
"appSettings_unitsMetric": "Metrico (m/km)",
"appSettings_unitsImperial": "Imperiale (ft / mi)",
"map_lineOfSight": "Linea di vista",
"map_losScreenTitle": "Linea di vista",
"losSelectStartEnd": "Seleziona i nodi iniziali e finali per la LOS.",
"losRunFailed": "Controllo della linea di vista fallito: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Cancella tutti i punti",
"losRunToViewElevationProfile": "Eseguire LOS per visualizzare il profilo altimetrico",
"losMenuTitle": "Menù LOS",
"losMenuSubtitle": "Tocca i nodi o premi a lungo la mappa per punti personalizzati",
"losShowDisplayNodes": "Mostra i nodi di visualizzazione",
"losCustomPoints": "Punti personalizzati",
"losCustomPointLabel": "Personalizzato {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Punto A",
"losPointB": "Punto B",
"losAntennaA": "Antenna A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antenna B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Esegui LOS",
"losNoElevationData": "Nessun dato di elevazione",
"losProfileClear": "{distance} {distanceUnit}, libera LOS, distanza minima {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, bloccato da {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: controllo...",
"losStatusNoData": "LOS: nessun dato",
"losStatusSummary": "LOS: {clear}/{total} libera, {blocked} bloccato, {unknown} sconosciuto",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Dati di elevazione non disponibili per uno o più campioni.",
"losErrorInvalidInput": "Dati punti/elevazione non validi per il calcolo della LOS.",
"losRenameCustomPoint": "Rinomina punto personalizzato",
"losPointName": "Nome del punto",
"losShowPanelTooltip": "Mostra il pannello LOS",
"losHidePanelTooltip": "Nascondi il pannello LOS",
"losElevationAttribution": "Dati di elevazione: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Orizzonte radio",
"losLegendLosBeam": "Linea di vista",
"losLegendTerrain": "Terreno",
"losFrequencyLabel": "Frequenza",
"losFrequencyInfoTooltip": "Visualizza i dettagli del calcolo",
"losFrequencyDialogTitle": "Calcolo dellorizzonte radio",
"losFrequencyDialogDescription": "Partendo da k={baselineK} a {baselineFreq} MHz, il calcolo regola il fattore k per l'attuale banda {frequencyMHz} MHz, che definisce il limite curvo dell'orizzonte radio.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_addToFavorites": "Aggiungi ai preferiti",
"listFilter_removeFromFavorites": "Rimuovi dai preferiti",
"listFilter_favorites": "Preferiti",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_searchUsers": "Cerca {number}{str} Utenti...",
"contacts_searchContactsNoNumber": "Cerca Contatti...",
"contacts_searchFavorites": "Cerca {number}{str} Preferiti...",
"contacts_unread": "Non letti",
"contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...",
"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"
"pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!"
}
File diff suppressed because it is too large Load Diff
+15 -495
View File
@@ -38,9 +38,6 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get common_delete => 'Изтрий';
@override
String get common_deleteAll => 'Изтрий всичко';
@override
String get common_close => 'Затвори';
@@ -111,134 +108,6 @@ 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 => 'Сканиране за устройства...';
@@ -274,23 +143,6 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get scanner_scan => 'Сканирай';
@override
String get scanner_bluetoothOff => 'Bluetooth е изключен.';
@override
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';
@override
String get device_quickSwitch => 'Бързо превключване';
@@ -372,13 +224,6 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get settings_longitude => 'Дължина';
@override
String get settings_contactSettings => 'Настройки за контакти';
@override
String get settings_contactSettingsSubtitle =>
'Настройки за добавяне на контакти.';
@override
String get settings_privacyMode => 'Режим на поверителност';
@@ -471,10 +316,6 @@ class AppLocalizationsBg extends AppLocalizations {
String get settings_aboutDescription =>
'Отворен софтуер за Flutter клиент за MeshCore LoRa мрежови устройства.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Име';
@@ -499,6 +340,15 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get settings_presets => 'Предварителни настройки';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Честота (MHz)';
@@ -527,15 +377,10 @@ class AppLocalizationsBg extends AppLocalizations {
String get settings_txPowerInvalid => 'Невалидна мощност на TX (0-22 dBm)';
@override
String get settings_clientRepeat => 'Без електричество – повторение';
String get settings_longRange => 'Дълъг обхват';
@override
String get settings_clientRepeatSubtitle =>
'Позволете на това устройство да предава пакети към мрежата за други устройства.';
@override
String get settings_clientRepeatFreqWarning =>
'За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.';
String get settings_fastSpeed => 'Бърза скорост';
@override
String settings_error(String message) {
@@ -611,14 +456,6 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get appSettings_languageUk => 'Украински';
@override
String get appSettings_enableMessageTracing =>
'Разрешаване на проследяване на съобщения';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Показване на подробни метаданни за маршрутизация и синхронизация за съобщения';
@override
String get appSettings_notifications => 'Уведомления';
@@ -779,15 +616,6 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Кеш на офлайн карти';
@override
String get appSettings_unitsTitle => 'единици';
@override
String get appSettings_unitsMetric => 'Метрика (m / km)';
@override
String get appSettings_unitsImperial => 'Имперска (ft / mi)';
@override
String get appSettings_noAreaSelected => 'Няма избрана област';
@@ -826,35 +654,7 @@ class AppLocalizationsBg extends AppLocalizations {
'Контактите ще се появят, когато устройствата рекламират.';
@override
String get contacts_unread => 'Непрочетено';
@override
String get contacts_searchContactsNoNumber => 'Търси контакти...';
@override
String contacts_searchContacts(int number, String str) {
return 'Търсене на контакти...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Търсене на $number$str любими...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Търсене на $number$str потребители...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Търсене на $number$str повтарящи се...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Търсене на $number$str сървъри в стаята...';
}
String get contacts_searchContacts => 'Търсене на контакти...';
@override
String get contacts_noUnreadContacts => 'Няма непрочетени контакти';
@@ -979,12 +779,6 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get channels_editChannel => 'Редактирай канал';
@override
String get channels_muteChannel => 'Заглуши канала';
@override
String get channels_unmuteChannel => 'Включи известията на канала';
@override
String get channels_deleteChannel => 'Изтрий канала';
@@ -993,11 +787,6 @@ class AppLocalizationsBg extends AppLocalizations {
return 'Изтрий \"$name\"? Това не може да бъде отменено.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Неуспешно изтриване на канала \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Каналът \"$name\" е изтрит';
@@ -1285,9 +1074,6 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get chat_pathManagement => 'Управление на пътища';
@override
String get chat_ShowAllPaths => 'Покажи всички пътища';
@override
String get chat_routingMode => 'Режим на маршрутизиране';
@@ -1448,12 +1234,6 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get map_title => 'Карта на възлите';
@override
String get map_lineOfSight => 'Линия на видимост';
@override
String get map_losScreenTitle => 'Линия на видимост';
@override
String get map_noNodesWithLocation => 'Няма възли с данни за местоположение.';
@@ -1511,9 +1291,6 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get map_shareMarkerHere => 'Споделете маркер тук';
@override
String get map_setAsMyLocation => 'Задайте като моя местоположение';
@override
String get map_pinLabel => 'Етикетиране на пин';
@@ -1574,16 +1351,6 @@ 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 => 'Последна видяна дата';
@@ -1596,19 +1363,6 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get map_manageRepeater => 'Управление на Повтарящ се Елемент';
@override
String get map_tapToAdd =>
'Натиснете върху възлите, за да ги добавите към пътя.';
@override
String get map_runTrace => 'Изпълни Път на Следване';
@override
String get map_removeLast => 'Премахни Последно';
@override
String get map_pathTraceCancelled => 'Отменен е следването на пътя.';
@override
String get mapCache_title => 'Кеш на офлайн карти';
@@ -1904,10 +1658,10 @@ class AppLocalizationsBg extends AppLocalizations {
String get repeater_cliSubtitle => 'Изпрати команди към ретранслатора';
@override
String get repeater_neighbors => 'Съседи';
String get repeater_neighbours => 'Съседи';
@override
String get repeater_neighborsSubtitle =>
String get repeater_neighboursSubtitle =>
'Преглед на съседни възли с нулев скок.';
@override
@@ -2607,7 +2361,7 @@ class AppLocalizationsBg extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbors => 'Повторители Съседи';
String get neighbors_repeatersNeighbours => 'Повторители Съседи';
@override
String get neighbors_noData => 'Няма налични данни за съседи.';
@@ -2914,15 +2668,6 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get listFilter_all => 'Всички';
@override
String get listFilter_favorites => 'Любими';
@override
String get listFilter_addToFavorites => 'Добави към любими';
@override
String get listFilter_removeFromFavorites => 'Премахване от списъка с любими';
@override
String get listFilter_users => 'Потребители';
@@ -2954,147 +2699,6 @@ class AppLocalizationsBg extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'Един или повече от хмелите липсва местоположение!';
@override
String get pathTrace_clearTooltip => 'Изчисти пътя';
@override
String get losSelectStartEnd => 'Изберете начални и крайни възли за LOS.';
@override
String losRunFailed(String error) {
return 'Проверката на пряката видимост е неуспешна: $error';
}
@override
String get losClearAllPoints => 'Изчистете всички точки';
@override
String get losRunToViewElevationProfile =>
'Стартирайте LOS, за да видите профила на надморската височина';
@override
String get losMenuTitle => 'LOS меню';
@override
String get losMenuSubtitle =>
'Докоснете възли или натиснете продължително карта за персонализирани точки';
@override
String get losShowDisplayNodes => 'Показване на възли на дисплея';
@override
String get losCustomPoints => 'Персонализирани точки';
@override
String losCustomPointLabel(int index) {
return 'Персонализирано $index';
}
@override
String get losPointA => 'Точка А';
@override
String get losPointB => 'Точка Б';
@override
String losAntennaA(String value, String unit) {
return 'Антена A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Антена B: $value $unit';
}
@override
String get losRun => 'Стартирайте LOS';
@override
String get losNoElevationData => 'Няма данни за надморска височина';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, чист LOS, минимално разстояние $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, блокиран от $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: проверка...';
@override
String get losStatusNoData => 'LOS: няма данни';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total ясно, $blocked блокирано, $unknown неизвестно';
}
@override
String get losErrorElevationUnavailable =>
'Няма налични данни за надморска височина за една или повече проби.';
@override
String get losErrorInvalidInput =>
'Невалидни данни за точки/надморска височина за изчисляване на LOS.';
@override
String get losRenameCustomPoint => 'Преименувайте персонализирана точка';
@override
String get losPointName => 'Име на точката';
@override
String get losShowPanelTooltip => 'Показване на LOS панел';
@override
String get losHidePanelTooltip => 'Скриване на LOS панела';
@override
String get losElevationAttribution =>
'Данни за надморска височина: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Радиохоризонт';
@override
String get losLegendLosBeam => 'Линия на видимост';
@override
String get losLegendTerrain => 'Терен';
@override
String get losFrequencyLabel => 'Честота';
@override
String get losFrequencyInfoTooltip => 'Преглед на детайли за изчислението';
@override
String get losFrequencyDialogTitle => 'Изчисляване на радиохоризонта';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Започвайки от k=$baselineK при $baselineFreq MHz, изчислението коригира k-фактора за текущата $frequencyMHz MHz лента, която определя границата на извития радиохоризонт.';
}
@override
String get contacts_pathTrace => 'Пътен проследяване';
@@ -3264,88 +2868,4 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'meshcore-open износ на данни за карта в формат GPX';
@override
String get snrIndicator_nearByRepeaters => 'Близки повтарящи се устройства';
@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 =>
'Сигурни ли сте, че искате да изтриете всички открити контакти?';
}
+21 -507
View File
@@ -38,9 +38,6 @@ 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';
@@ -111,137 +108,6 @@ 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...';
@@ -277,23 +143,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get scanner_scan => 'Scannen';
@override
String get scanner_bluetoothOff => 'Bluetooth ist deaktiviert.';
@override
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';
@override
String get device_quickSwitch => 'Schnelles Umschalten';
@@ -374,13 +223,6 @@ 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';
@@ -468,10 +310,6 @@ class AppLocalizationsDe extends AppLocalizations {
String get settings_aboutDescription =>
'Ein Open-Source-Flutter-Client für MeshCore LoRa-Meshnetzwerkgeräte.';
@override
String get settings_aboutOpenMeteoAttribution =>
'LOS-Höhendaten: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Name';
@@ -496,6 +334,15 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get settings_presets => 'Voreinstellungen';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frequenz (MHz)';
@@ -524,15 +371,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get settings_txPowerInvalid => 'Ungültige TX-Leistung (0-22 dBm)';
@override
String get settings_clientRepeat => 'Wiederholung, ohne Stromanschluss';
String get settings_longRange => 'Grosse Reichweite';
@override
String get settings_clientRepeatSubtitle =>
'Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen.';
@override
String get settings_clientRepeatFreqWarning =>
'Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.';
String get settings_fastSpeed => 'Schnelle Geschwindigkeit';
@override
String settings_error(String message) {
@@ -608,14 +450,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get appSettings_languageUk => 'Ukrainisch';
@override
String get appSettings_enableMessageTracing =>
'Nachrichtenverfolgung aktivieren';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen';
@override
String get appSettings_notifications => 'Benachrichtigungen';
@@ -779,15 +613,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Offline-Karten-Cache';
@override
String get appSettings_unitsTitle => 'Einheiten';
@override
String get appSettings_unitsMetric => 'Metrisch (m/km)';
@override
String get appSettings_unitsImperial => 'Imperial (ft/mi)';
@override
String get appSettings_noAreaSelected => 'Kein Bereich ausgewählt';
@@ -825,35 +650,7 @@ class AppLocalizationsDe extends AppLocalizations {
'Kontakte werden angezeigt, wenn Geräte eine Ankündigung machen.';
@override
String get contacts_unread => 'Ungelesen';
@override
String get contacts_searchContactsNoNumber => 'Kontakte suchen...';
@override
String contacts_searchContacts(int number, String str) {
return 'Suche Kontakte...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Suche $number$str Favoriten...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Suche $number$str Benutzer...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Suche $number$str Repeater...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Suche $number$str Raumserver...';
}
String get contacts_searchContacts => 'Suche Kontakte...';
@override
String get contacts_noUnreadContacts => 'Keine ungesehene Kontakte';
@@ -918,27 +715,27 @@ class AppLocalizationsDe extends AppLocalizations {
String get contacts_noMembers => 'Keine Mitglieder';
@override
String get contacts_lastSeenNow => 'kürzlich';
String get contacts_lastSeenNow => 'gerade gesehen';
@override
String contacts_lastSeenMinsAgo(int minutes) {
return '~ $minutes Min.';
return 'Letzte Sichtung vor $minutes Minuten.';
}
@override
String get contacts_lastSeenHourAgo => '~ 1 Std.';
String get contacts_lastSeenHourAgo => 'Letzte Sichtung vor 1 Stunde.';
@override
String contacts_lastSeenHoursAgo(int hours) {
return '~ $hours Std.';
return 'Letzte Sichtung vor $hours Stunden.';
}
@override
String get contacts_lastSeenDayAgo => '~ 1 Tag';
String get contacts_lastSeenDayAgo => 'Letzte Sichtung vor 1 Tag';
@override
String contacts_lastSeenDaysAgo(int days) {
return '~ $days Tage';
return 'Letzte Sichtung $days Tage zuvor';
}
@override
@@ -979,12 +776,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get channels_editChannel => 'Kanal bearbeiten';
@override
String get channels_muteChannel => 'Kanal stummschalten';
@override
String get channels_unmuteChannel => 'Kanal Stummschaltung aufheben';
@override
String get channels_deleteChannel => 'Lösche den Kanal';
@@ -993,11 +784,6 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Löschen von \"$name\"? Dies kann nicht rückgängig gemacht werden.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Kanal $name konnte nicht gelöscht werden';
}
@override
String channels_channelDeleted(String name) {
return 'Kanal \"$name\" gelöscht';
@@ -1288,9 +1074,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get chat_pathManagement => 'Pfadverwaltung';
@override
String get chat_ShowAllPaths => 'Alle Pfade anzeigen';
@override
String get chat_routingMode => 'Routenmodus';
@@ -1450,12 +1233,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get map_title => 'Karte';
@override
String get map_lineOfSight => 'Sichtlinie';
@override
String get map_losScreenTitle => 'Sichtlinie';
@override
String get map_noNodesWithLocation => 'Keine Knoten mit Standortdaten';
@@ -1513,9 +1290,6 @@ 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';
@@ -1576,16 +1350,6 @@ 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';
@@ -1598,19 +1362,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get map_manageRepeater => 'Repeater verwalten';
@override
String get map_tapToAdd =>
'Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.';
@override
String get map_runTrace => 'Pfadverlauf ausführen';
@override
String get map_removeLast => 'Letztes Entfernen';
@override
String get map_pathTraceCancelled => 'Pfadverfolgung abgebrochen.';
@override
String get mapCache_title => 'Offline-Karten-Cache';
@@ -1906,10 +1657,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get repeater_cliSubtitle => 'Sende Befehle an den Repeater';
@override
String get repeater_neighbors => 'Nachbarn';
String get repeater_neighbours => 'Nachbarn';
@override
String get repeater_neighborsSubtitle => 'Anzahl der Hop-Nachbarn anzeigen.';
String get repeater_neighboursSubtitle => 'Anzahl der Hop-Nachbarn anzeigen.';
@override
String get repeater_settings => 'Einstellungen';
@@ -2612,7 +2363,7 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbors => 'Nachbarn';
String get neighbors_repeatersNeighbours => 'Nachbarn';
@override
String get neighbors_noData => 'Keine Nachbarsdaten verfügbar.';
@@ -2922,15 +2673,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get listFilter_all => 'Alle';
@override
String get listFilter_favorites => 'Favoriten';
@override
String get listFilter_addToFavorites => 'Zu Favoriten hinzufügen';
@override
String get listFilter_removeFromFavorites => 'Aus Favoriten entfernen';
@override
String get listFilter_users => 'Benutzer';
@@ -2962,148 +2704,6 @@ class AppLocalizationsDe extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'Bei einer oder mehreren Knoten fehlt der Standort!';
@override
String get pathTrace_clearTooltip => 'Pfad löschen';
@override
String get losSelectStartEnd =>
'Wählen Sie Start- und Endknoten für LOS aus.';
@override
String losRunFailed(String error) {
return 'Sichtlinienprüfung fehlgeschlagen: $error';
}
@override
String get losClearAllPoints => 'Löschen Sie alle Punkte';
@override
String get losRunToViewElevationProfile =>
'Führen Sie LOS aus, um das Höhenprofil anzuzeigen';
@override
String get losMenuTitle => 'LOS-Menü';
@override
String get losMenuSubtitle =>
'Tippen Sie auf Knoten oder drücken Sie lange auf die Karte, um benutzerdefinierte Punkte anzuzeigen';
@override
String get losShowDisplayNodes => 'Anzeigeknoten anzeigen';
@override
String get losCustomPoints => 'Benutzerdefinierte Punkte';
@override
String losCustomPointLabel(int index) {
return 'Benutzerdefiniert $index';
}
@override
String get losPointA => 'Punkt A';
@override
String get losPointB => 'Punkt B';
@override
String losAntennaA(String value, String unit) {
return 'Antenne A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antenne B: $value $unit';
}
@override
String get losRun => 'Führen Sie LOS aus';
@override
String get losNoElevationData => 'Keine Höhendaten';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, freie Sichtlinie, Mindestabstand $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, blockiert durch $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: Überprüfen...';
@override
String get losStatusNoData => 'LOS: keine Daten';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'Sichtlinie: $clear/$total frei, $blocked blockiert, $unknown unbekannt';
}
@override
String get losErrorElevationUnavailable =>
'Für eine oder mehrere Proben sind keine Höhendaten verfügbar.';
@override
String get losErrorInvalidInput =>
'Ungültige Punkte/Höhendaten für die LOS-Berechnung.';
@override
String get losRenameCustomPoint =>
'Benennen Sie den benutzerdefinierten Punkt um';
@override
String get losPointName => 'Punktname';
@override
String get losShowPanelTooltip => 'LOS-Panel anzeigen';
@override
String get losHidePanelTooltip => 'LOS-Panel ausblenden';
@override
String get losElevationAttribution => 'Höhendaten: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Funkhorizont';
@override
String get losLegendLosBeam => 'Sichtlinie';
@override
String get losLegendTerrain => 'Gelände';
@override
String get losFrequencyLabel => 'Frequenz';
@override
String get losFrequencyInfoTooltip => 'Details zur Berechnung anzeigen';
@override
String get losFrequencyDialogTitle => 'Berechnung des Funkhorizonts';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Ausgehend von k=$baselineK bei $baselineFreq MHz passt die Berechnung den k-Faktor für das aktuelle $frequencyMHz MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.';
}
@override
String get contacts_pathTrace => 'Pfadverfolgung';
@@ -3276,90 +2876,4 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'GPX-Kartendaten aus meshcore-open exportieren';
@override
String get snrIndicator_nearByRepeaters => 'In der Nähe befindliche Repeater';
@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?';
}
+24 -494
View File
@@ -38,9 +38,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get common_delete => 'Delete';
@override
String get common_deleteAll => 'Delete All';
@override
String get common_close => 'Close';
@@ -111,132 +108,6 @@ 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...';
@@ -271,23 +142,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get scanner_scan => 'Scan';
@override
String get scanner_bluetoothOff => 'Bluetooth is off';
@override
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';
@override
String get device_quickSwitch => 'Quick switch';
@@ -368,13 +222,6 @@ 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';
@@ -461,10 +308,6 @@ class AppLocalizationsEn extends AppLocalizations {
String get settings_aboutDescription =>
'An open-source Flutter client for MeshCore LoRa mesh networking devices.';
@override
String get settings_aboutOpenMeteoAttribution =>
'LOS elevation data: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Name';
@@ -489,6 +332,15 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settings_presets => 'Presets';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frequency (MHz)';
@@ -517,15 +369,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get settings_txPowerInvalid => 'Invalid TX power (0-22 dBm)';
@override
String get settings_clientRepeat => 'Off-Grid Repeat';
String get settings_longRange => 'Long Range';
@override
String get settings_clientRepeatSubtitle =>
'Allow this device to repeat mesh packets for others';
@override
String get settings_clientRepeatFreqWarning =>
'Off-grid repeat requires 433, 869, or 918 MHz frequency';
String get settings_fastSpeed => 'Fast Speed';
@override
String settings_error(String message) {
@@ -601,13 +448,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get appSettings_languageUk => 'Українська';
@override
String get appSettings_enableMessageTracing => 'Enable Message Tracing';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Show detailed routing and timing metadata for messages';
@override
String get appSettings_notifications => 'Notifications';
@@ -768,15 +608,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Offline Map Cache';
@override
String get appSettings_unitsTitle => 'Units';
@override
String get appSettings_unitsMetric => 'Metric (m / km)';
@override
String get appSettings_unitsImperial => 'Imperial (ft / mi)';
@override
String get appSettings_noAreaSelected => 'No area selected';
@@ -813,35 +644,7 @@ class AppLocalizationsEn extends AppLocalizations {
'Contacts will appear when devices advertise';
@override
String get contacts_unread => 'Unread';
@override
String get contacts_searchContactsNoNumber => 'Search Contacts...';
@override
String contacts_searchContacts(int number, String str) {
return 'Search $number$str Contacts...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Search $number$str Favorites...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Search $number$str Users...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Search $number$str Repeaters...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Search $number$str Room servers...';
}
String get contacts_searchContacts => 'Search contacts...';
@override
String get contacts_noUnreadContacts => 'No unread contacts';
@@ -904,27 +707,27 @@ class AppLocalizationsEn extends AppLocalizations {
String get contacts_noMembers => 'No members';
@override
String get contacts_lastSeenNow => 'recently';
String get contacts_lastSeenNow => 'Last seen now';
@override
String contacts_lastSeenMinsAgo(int minutes) {
return '~ $minutes min.';
return 'Last seen $minutes mins ago';
}
@override
String get contacts_lastSeenHourAgo => '~ 1 hour';
String get contacts_lastSeenHourAgo => 'Last seen 1 hour ago';
@override
String contacts_lastSeenHoursAgo(int hours) {
return '~ $hours hours';
return 'Last seen $hours hours ago';
}
@override
String get contacts_lastSeenDayAgo => '~ 1 day';
String get contacts_lastSeenDayAgo => 'Last seen 1 day ago';
@override
String contacts_lastSeenDaysAgo(int days) {
return '~ $days days';
return 'Last seen $days days ago';
}
@override
@@ -965,12 +768,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get channels_editChannel => 'Edit channel';
@override
String get channels_muteChannel => 'Mute channel';
@override
String get channels_unmuteChannel => 'Unmute channel';
@override
String get channels_deleteChannel => 'Delete channel';
@@ -979,11 +776,6 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Delete \"$name\"? This cannot be undone.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Failed to delete channel \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Channel \"$name\" deleted';
@@ -1267,9 +1059,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get chat_pathManagement => 'Path Management';
@override
String get chat_ShowAllPaths => 'Show all paths';
@override
String get chat_routingMode => 'Routing mode';
@@ -1424,12 +1213,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get map_title => 'Node Map';
@override
String get map_lineOfSight => 'Line of Sight';
@override
String get map_losScreenTitle => 'Line of Sight';
@override
String get map_noNodesWithLocation => 'No nodes with location data';
@@ -1487,9 +1270,6 @@ 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';
@@ -1550,15 +1330,6 @@ 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';
@@ -1571,18 +1342,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get map_manageRepeater => 'Manage Repeater';
@override
String get map_tapToAdd => 'Tap on nodes to add them to the path.';
@override
String get map_runTrace => 'Run Path Trace';
@override
String get map_removeLast => 'Remove Last';
@override
String get map_pathTraceCancelled => 'Path trace cancelled.';
@override
String get mapCache_title => 'Offline Map Cache';
@@ -1873,10 +1632,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get repeater_cliSubtitle => 'Send commands to the repeater';
@override
String get repeater_neighbors => 'Neighbors';
String get repeater_neighbours => 'Neighbors';
@override
String get repeater_neighborsSubtitle => 'View zero hop neighbors.';
String get repeater_neighboursSubtitle => 'View zero hop neighbors.';
@override
String get repeater_settings => 'Settings';
@@ -2552,10 +2311,10 @@ class AppLocalizationsEn extends AppLocalizations {
}
@override
String get neighbors_receivedData => 'Received Neighbors Data';
String get neighbors_receivedData => 'Received Neighbours Data';
@override
String get neighbors_requestTimedOut => 'Neighbors request timed out.';
String get neighbors_requestTimedOut => 'Neighbours request timed out.';
@override
String neighbors_errorLoading(String error) {
@@ -2563,10 +2322,10 @@ class AppLocalizationsEn extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbors => 'Repeaters Neighbors';
String get neighbors_repeatersNeighbours => 'Repeaters Neighbours';
@override
String get neighbors_noData => 'No neighbors data available.';
String get neighbors_noData => 'No neighbours data available.';
@override
String neighbors_unknownContact(String pubkey) {
@@ -2869,15 +2628,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get listFilter_all => 'All';
@override
String get listFilter_favorites => 'Favorites';
@override
String get listFilter_addToFavorites => 'Add to favorites';
@override
String get listFilter_removeFromFavorites => 'Remove from favorites';
@override
String get listFilter_users => 'Users';
@@ -2909,146 +2659,6 @@ class AppLocalizationsEn extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'One or more of the hops is missing a location!';
@override
String get pathTrace_clearTooltip => 'Clear path.';
@override
String get losSelectStartEnd => 'Select start and end nodes for LOS.';
@override
String losRunFailed(String error) {
return 'Line-of-sight check failed: $error';
}
@override
String get losClearAllPoints => 'Clear all points';
@override
String get losRunToViewElevationProfile =>
'Run LOS to view elevation profile';
@override
String get losMenuTitle => 'LOS Menu';
@override
String get losMenuSubtitle => 'Tap nodes or long-press map for custom points';
@override
String get losShowDisplayNodes => 'Show display nodes';
@override
String get losCustomPoints => 'Custom points';
@override
String losCustomPointLabel(int index) {
return 'Custom $index';
}
@override
String get losPointA => 'Point A';
@override
String get losPointB => 'Point B';
@override
String losAntennaA(String value, String unit) {
return 'Antenna A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antenna B: $value $unit';
}
@override
String get losRun => 'Run LOS';
@override
String get losNoElevationData => 'No elevation data';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, clear LOS, min clearance $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, blocked by $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: checking...';
@override
String get losStatusNoData => 'LOS: no data';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total clear, $blocked blocked, $unknown unknown';
}
@override
String get losErrorElevationUnavailable =>
'Elevation data unavailable for one or more samples.';
@override
String get losErrorInvalidInput =>
'Invalid points/elevation data for LOS calculation.';
@override
String get losRenameCustomPoint => 'Rename custom point';
@override
String get losPointName => 'Point name';
@override
String get losShowPanelTooltip => 'Show LOS panel';
@override
String get losHidePanelTooltip => 'Hide LOS panel';
@override
String get losElevationAttribution =>
'Elevation data: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Radio horizon';
@override
String get losLegendLosBeam => 'LOS beam';
@override
String get losLegendTerrain => 'Terrain';
@override
String get losFrequencyLabel => 'Frequency';
@override
String get losFrequencyInfoTooltip => 'View calculation details';
@override
String get losFrequencyDialogTitle => 'Radio horizon calculation';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation adjusts the k-factor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.';
}
@override
String get contacts_pathTrace => 'Path Trace';
@@ -3214,84 +2824,4 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'meshcore-open GPX map data export';
@override
String get snrIndicator_nearByRepeaters => 'Nearby Repeaters';
@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?';
}
+20 -505
View File
@@ -38,9 +38,6 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get common_delete => 'Eliminar';
@override
String get common_deleteAll => 'Eliminar todo';
@override
String get common_close => 'Cerrar';
@@ -111,135 +108,6 @@ 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...';
@@ -275,23 +143,6 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get scanner_scan => 'Escanea';
@override
String get scanner_bluetoothOff => 'Bluetooth está desactivado.';
@override
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';
@override
String get device_quickSwitch => 'Cambiar rápidamente';
@@ -372,13 +223,6 @@ 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';
@@ -469,10 +313,6 @@ class AppLocalizationsEs extends AppLocalizations {
String get settings_aboutDescription =>
'Un cliente de código abierto de Flutter para dispositivos de red mesh LoRa de MeshCore.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Datos de elevación LOS: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Nombre';
@@ -497,6 +337,15 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get settings_presets => 'Preajustes';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frecuencia (MHz)';
@@ -525,15 +374,10 @@ class AppLocalizationsEs extends AppLocalizations {
String get settings_txPowerInvalid => 'Potencia de TX inválida (0-22 dBm)';
@override
String get settings_clientRepeat => 'Repetir sin conexión';
String get settings_longRange => 'Largo Alcance';
@override
String get settings_clientRepeatSubtitle =>
'Permita que este dispositivo repita los paquetes de red para otros usuarios.';
@override
String get settings_clientRepeatFreqWarning =>
'Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.';
String get settings_fastSpeed => 'Velocidad Rápida';
@override
String settings_error(String message) {
@@ -609,14 +453,6 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get appSettings_languageUk => 'Ucraniano';
@override
String get appSettings_enableMessageTracing =>
'Habilitar seguimiento de mensajes';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Mostrar metadatos detallados de enrutamiento y tiempo para los mensajes';
@override
String get appSettings_notifications => 'Notificaciones';
@@ -778,15 +614,6 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Caché de Mapa Offline';
@override
String get appSettings_unitsTitle => 'Unidades';
@override
String get appSettings_unitsMetric => 'Métrico (m/km)';
@override
String get appSettings_unitsImperial => 'Imperial (pies/millas)';
@override
String get appSettings_noAreaSelected => 'No se ha seleccionado ningún área';
@@ -824,35 +651,7 @@ class AppLocalizationsEs extends AppLocalizations {
'Los contactos aparecerán cuando los dispositivos anuncien.';
@override
String get contacts_unread => 'No leído';
@override
String get contacts_searchContactsNoNumber => 'Buscar contactos...';
@override
String contacts_searchContacts(int number, String str) {
return 'Buscar contactos...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Buscar $number$str Favoritos...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Buscar $number$str Usuarios...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Buscar $number$str Repetidores...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Buscar $number$str servidores de sala...';
}
String get contacts_searchContacts => 'Buscar contactos...';
@override
String get contacts_noUnreadContacts => 'No contactos sin leer';
@@ -921,23 +720,23 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String contacts_lastSeenMinsAgo(int minutes) {
return '~ $minutes min.';
return 'Última vez visto hace $minutes minutos.';
}
@override
String get contacts_lastSeenHourAgo => '~ 1 hora';
String get contacts_lastSeenHourAgo => 'Última vez que se vio hace 1 hora';
@override
String contacts_lastSeenHoursAgo(int hours) {
return '~ $hours horas';
return 'Última vez visto hace $hours horas.';
}
@override
String get contacts_lastSeenDayAgo => '~ 1 día';
String get contacts_lastSeenDayAgo => 'Última vez que se vio hace 1 día';
@override
String contacts_lastSeenDaysAgo(int days) {
return '~ $days días';
return 'Última vez visto hace $days días.';
}
@override
@@ -978,12 +777,6 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get channels_editChannel => 'Editar canal';
@override
String get channels_muteChannel => 'Silenciar canal';
@override
String get channels_unmuteChannel => 'Activar canal';
@override
String get channels_deleteChannel => 'Eliminar canal';
@@ -992,11 +785,6 @@ class AppLocalizationsEs extends AppLocalizations {
return 'Eliminar \"$name\"? Esto no se puede deshacer.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'No se pudo eliminar el canal \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Canal \"$name\" eliminado';
@@ -1285,9 +1073,6 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get chat_pathManagement => 'Gestión de Rutas';
@override
String get chat_ShowAllPaths => 'Mostrar todos los caminos';
@override
String get chat_routingMode => 'Modo de enrutamiento';
@@ -1446,12 +1231,6 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get map_title => 'Mapa de Nodos';
@override
String get map_lineOfSight => 'Línea de visión';
@override
String get map_losScreenTitle => 'Línea de visión';
@override
String get map_noNodesWithLocation => 'No hay nodos con datos de ubicación';
@@ -1509,9 +1288,6 @@ 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';
@@ -1572,16 +1348,6 @@ 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';
@@ -1594,18 +1360,6 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get map_manageRepeater => 'Gestionar Repetidor';
@override
String get map_tapToAdd => 'Pulse en los nodos para agregarlos al camino.';
@override
String get map_runTrace => 'Ejecutar Rastreo de Ruta';
@override
String get map_removeLast => 'Eliminar último';
@override
String get map_pathTraceCancelled => 'Rastreo de ruta cancelado.';
@override
String get mapCache_title => 'Caché de Mapa Offline';
@@ -1902,10 +1656,10 @@ class AppLocalizationsEs extends AppLocalizations {
String get repeater_cliSubtitle => 'Enviar comandos al repetidor';
@override
String get repeater_neighbors => 'Vecinos';
String get repeater_neighbours => 'Vecinos';
@override
String get repeater_neighborsSubtitle => 'Ver vecinos de salto cero.';
String get repeater_neighboursSubtitle => 'Ver vecinos de salto cero.';
@override
String get repeater_settings => 'Configuración';
@@ -2604,7 +2358,7 @@ class AppLocalizationsEs extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbors => 'Repetidores Vecinos';
String get neighbors_repeatersNeighbours => 'Repetidores Vecinos';
@override
String get neighbors_noData => 'No hay datos de vecinos disponibles.';
@@ -2913,15 +2667,6 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get listFilter_all => 'Todas';
@override
String get listFilter_favorites => 'Favoritos';
@override
String get listFilter_addToFavorites => 'Añadir a favoritos';
@override
String get listFilter_removeFromFavorites => 'Eliminar de las favoritas';
@override
String get listFilter_users => 'Usuarios';
@@ -2953,149 +2698,6 @@ class AppLocalizationsEs extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'Uno o más de los lúpulos carecen de una ubicación';
@override
String get pathTrace_clearTooltip => 'Borrar ruta';
@override
String get losSelectStartEnd =>
'Seleccione los nodos de inicio y fin para LOS.';
@override
String losRunFailed(String error) {
return 'Error en la comprobación de la línea de visión: $error';
}
@override
String get losClearAllPoints => 'Borrar todos los puntos';
@override
String get losRunToViewElevationProfile =>
'Ejecute LOS para ver el perfil de elevación';
@override
String get losMenuTitle => 'Menú LOS';
@override
String get losMenuSubtitle =>
'Toque nodos o mantenga presionado el mapa para puntos personalizados';
@override
String get losShowDisplayNodes => 'Mostrar nodos de visualización';
@override
String get losCustomPoints => 'Puntos personalizados';
@override
String losCustomPointLabel(int index) {
return 'Personalizado $index';
}
@override
String get losPointA => 'Punto A';
@override
String get losPointB => 'Punto B';
@override
String losAntennaA(String value, String unit) {
return 'Antena A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antena B: $value $unit';
}
@override
String get losRun => 'Ejecutar LOS';
@override
String get losNoElevationData => 'Sin datos de elevación';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, despejar LOS, autorización mínima $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, bloqueado por $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: comprobando...';
@override
String get losStatusNoData => 'LOS: sin datos';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total claro, $blocked bloqueado, $unknown desconocido';
}
@override
String get losErrorElevationUnavailable =>
'Datos de elevación no disponibles para una o más muestras.';
@override
String get losErrorInvalidInput =>
'Datos de puntos/elevación no válidos para el cálculo de LOS.';
@override
String get losRenameCustomPoint =>
'Cambiar el nombre del punto personalizado';
@override
String get losPointName => 'Nombre del punto';
@override
String get losShowPanelTooltip => 'Mostrar panel LOS';
@override
String get losHidePanelTooltip => 'Ocultar panel LOS';
@override
String get losElevationAttribution =>
'Datos de elevación: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Horizonte radioeléctrico';
@override
String get losLegendLosBeam => 'Línea de visión';
@override
String get losLegendTerrain => 'Terreno';
@override
String get losFrequencyLabel => 'Frecuencia';
@override
String get losFrequencyInfoTooltip => 'Ver detalles del cálculo';
@override
String get losFrequencyDialogTitle => 'Cálculo del horizonte radioeléctrico';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'A partir de k=$baselineK en $baselineFreq MHz, el cálculo ajusta el factor k para la banda actual de $frequencyMHz MHz, que define el límite curvo del horizonte de radio.';
}
@override
String get contacts_pathTrace => 'Rastreo de caminos';
@@ -3266,91 +2868,4 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'meshcore-open exportación de datos de mapa GPX';
@override
String get snrIndicator_nearByRepeaters => 'Repetidores cercanos';
@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!';
}
+24 -509
View File
@@ -38,9 +38,6 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get common_delete => 'Supprimer';
@override
String get common_deleteAll => 'Supprimer tout';
@override
String get common_close => 'Fermer';
@@ -111,137 +108,6 @@ 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...';
@@ -277,23 +143,6 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get scanner_scan => 'Scanner';
@override
String get scanner_bluetoothOff => 'Le Bluetooth est désactivé.';
@override
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';
@override
String get device_quickSwitch => 'Basculement rapide';
@@ -375,13 +224,6 @@ 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é';
@@ -472,10 +314,6 @@ class AppLocalizationsFr extends AppLocalizations {
String get settings_aboutDescription =>
'Un client Flutter open source pour les appareils de réseau mesh MeshCore LoRa.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Données d\'élévation LOS : Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Nom';
@@ -500,6 +338,15 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get settings_presets => 'Préréglages';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Fréquence (MHz)';
@@ -528,15 +375,10 @@ class AppLocalizationsFr extends AppLocalizations {
String get settings_txPowerInvalid => 'Puissance TX invalide (0-22 dBm)';
@override
String get settings_clientRepeat => 'Répétition hors réseau';
String get settings_longRange => 'Portée Longue';
@override
String get settings_clientRepeatSubtitle =>
'Permettez à cet appareil de répéter les paquets de données pour les autres.';
@override
String get settings_clientRepeatFreqWarning =>
'Pour les transmissions hors réseau, il est nécessaire d\'utiliser les fréquences de 433, 869 ou 918 MHz.';
String get settings_fastSpeed => 'Vitesse Rapide';
@override
String settings_error(String message) {
@@ -612,14 +454,6 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get appSettings_languageUk => 'Ukrainien';
@override
String get appSettings_enableMessageTracing =>
'Activer le traçage des messages';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Afficher les métadonnées détaillées de routage et de synchronisation des messages';
@override
String get appSettings_notifications => 'Notifications';
@@ -782,15 +616,6 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Cache de Carte Hors Ligne';
@override
String get appSettings_unitsTitle => 'Unités';
@override
String get appSettings_unitsMetric => 'Métrique (m/km)';
@override
String get appSettings_unitsImperial => 'Impérial (ft / mi)';
@override
String get appSettings_noAreaSelected => 'Aucune zone sélectionnée';
@@ -829,35 +654,7 @@ class AppLocalizationsFr extends AppLocalizations {
'Les contacts apparaîtront lorsque les appareils font leur annonce.';
@override
String get contacts_unread => 'Non lu';
@override
String get contacts_searchContactsNoNumber => 'Rechercher des contacts...';
@override
String contacts_searchContacts(int number, String str) {
return 'Rechercher des contacts...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Rechercher $number$str Favoris...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Rechercher $number$str utilisateurs...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Rechercher $number$str Répéteurs...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Rechercher $number$str serveurs de salle...';
}
String get contacts_searchContacts => 'Rechercher des contacts...';
@override
String get contacts_noUnreadContacts => 'Aucun contact non lu';
@@ -880,7 +677,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get contacts_manageRoom => 'Gérer le Room Server';
@override
String get contacts_roomLogin => 'Connexion Room Server';
String get contacts_roomLogin => 'Connexion Salle';
@override
String get contacts_openChat => 'Ouverture du Chat';
@@ -925,23 +722,23 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String contacts_lastSeenMinsAgo(int minutes) {
return '~ $minutes min.';
return 'Vu il y a $minutes minutes';
}
@override
String get contacts_lastSeenHourAgo => '~ 1 heure';
String get contacts_lastSeenHourAgo => 'Vu il y a 1 heure';
@override
String contacts_lastSeenHoursAgo(int hours) {
return '~ $hours heures';
return 'Vu il y a $hours heures';
}
@override
String get contacts_lastSeenDayAgo => '~ 1 jour';
String get contacts_lastSeenDayAgo => 'Vu il y a 1 jour';
@override
String contacts_lastSeenDaysAgo(int days) {
return '~ $days jours';
return 'Vu il y a $days jours';
}
@override
@@ -982,12 +779,6 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get channels_editChannel => 'Modifier le canal';
@override
String get channels_muteChannel => 'Désactiver les notifications du canal';
@override
String get channels_unmuteChannel => 'Réactiver les notifications du canal';
@override
String get channels_deleteChannel => 'Supprimer le canal';
@@ -996,11 +787,6 @@ class AppLocalizationsFr extends AppLocalizations {
return 'Supprimer $name? Cela ne peut pas être annulé.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Échec de la suppression de la chaîne \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Le canal \"$name\" a été supprimé';
@@ -1290,9 +1076,6 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get chat_pathManagement => 'Gestion des chemins';
@override
String get chat_ShowAllPaths => 'Afficher tous les chemins';
@override
String get chat_routingMode => 'Mode de routage';
@@ -1454,12 +1237,6 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get map_title => 'Carte des nœuds';
@override
String get map_lineOfSight => 'Ligne de vue';
@override
String get map_losScreenTitle => 'Ligne de vue';
@override
String get map_noNodesWithLocation =>
'Aucun nœud avec des données de localisation';
@@ -1518,9 +1295,6 @@ 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';
@@ -1581,16 +1355,6 @@ 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';
@@ -1603,19 +1367,6 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get map_manageRepeater => 'Gérer le répéteur';
@override
String get map_tapToAdd =>
'Appuyez sur les nœuds pour les ajouter au chemin.';
@override
String get map_runTrace => 'Exécuter la traçage de chemin';
@override
String get map_removeLast => 'Supprimer le dernier';
@override
String get map_pathTraceCancelled => 'Traçage de chemin annulé';
@override
String get mapCache_title => 'Cache de Carte Hors Ligne';
@@ -1761,7 +1512,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get login_repeaterLogin => 'Connexion au répéteur';
@override
String get login_roomLogin => 'Connexion Room Server';
String get login_roomLogin => 'Connexion Salle';
@override
String get login_password => 'Mot de passe';
@@ -1886,7 +1637,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get repeater_management => 'Gestion des répéteurs';
@override
String get room_management => 'Administrattion Room Server';
String get room_management => 'Administración del Servidor de Habitación';
@override
String get repeater_managementTools => 'Outils de Gestion';
@@ -1912,10 +1663,11 @@ class AppLocalizationsFr extends AppLocalizations {
String get repeater_cliSubtitle => 'Envoyer des commandes au répéteur';
@override
String get repeater_neighbors => 'Voisins';
String get repeater_neighbours => 'Voisins';
@override
String get repeater_neighborsSubtitle => 'Afficher les voisins de saut nuls.';
String get repeater_neighboursSubtitle =>
'Afficher les voisins de saut nuls.';
@override
String get repeater_settings => 'Paramètres';
@@ -2620,7 +2372,7 @@ class AppLocalizationsFr extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbors => 'Répéteurs Voisins';
String get neighbors_repeatersNeighbours => 'Répéteurs Voisins';
@override
String get neighbors_noData =>
@@ -2931,15 +2683,6 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get listFilter_all => 'Tout';
@override
String get listFilter_favorites => 'Préférences';
@override
String get listFilter_addToFavorites => 'Ajouter à mes favoris';
@override
String get listFilter_removeFromFavorites => 'Supprimer des favoris';
@override
String get listFilter_users => 'Utilisateurs';
@@ -2971,148 +2714,6 @@ class AppLocalizationsFr extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'Un ou plusieurs des sauts manquent d\'une localisation !';
@override
String get pathTrace_clearTooltip => 'Effacer le chemin';
@override
String get losSelectStartEnd =>
'Sélectionnez les nœuds de début et de fin pour LOS.';
@override
String losRunFailed(String error) {
return 'Échec de la vérification de la ligne de vue : $error';
}
@override
String get losClearAllPoints => 'Effacer tous les points';
@override
String get losRunToViewElevationProfile =>
'Exécutez LOS pour afficher le profil d\'altitude';
@override
String get losMenuTitle => 'Menu LOS';
@override
String get losMenuSubtitle =>
'Appuyez sur les nœuds ou appuyez longuement sur la carte pour des points personnalisés';
@override
String get losShowDisplayNodes => 'Afficher les nœuds d\'affichage';
@override
String get losCustomPoints => 'Points personnalisés';
@override
String losCustomPointLabel(int index) {
return 'Personnalisé $index';
}
@override
String get losPointA => 'Point A';
@override
String get losPointB => 'Point B';
@override
String losAntennaA(String value, String unit) {
return 'Antenne A : $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antenne B : $value $unit';
}
@override
String get losRun => 'Exécuter la LOS';
@override
String get losNoElevationData => 'Aucune donnée d\'altitude';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, LOS clair, clairance minimale $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, bloqué par $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS : vérification...';
@override
String get losStatusNoData => 'LOS : aucune donnée';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS : $clear/$total clair, $blocked bloqué, $unknown inconnu';
}
@override
String get losErrorElevationUnavailable =>
'Données d\'altitude indisponibles pour un ou plusieurs échantillons.';
@override
String get losErrorInvalidInput =>
'Données de points/d\'altitude non valides pour le calcul de la LOS.';
@override
String get losRenameCustomPoint => 'Renommer le point personnalisé';
@override
String get losPointName => 'Nom du point';
@override
String get losShowPanelTooltip => 'Afficher le panneau LOS';
@override
String get losHidePanelTooltip => 'Masquer le panneau LOS';
@override
String get losElevationAttribution =>
'Données daltitude : Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Horizon radio';
@override
String get losLegendLosBeam => 'Ligne de visée';
@override
String get losLegendTerrain => 'Terrain';
@override
String get losFrequencyLabel => 'Fréquence';
@override
String get losFrequencyInfoTooltip => 'Voir les détails du calcul';
@override
String get losFrequencyDialogTitle => 'Calcul de lhorizon radio';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'À partir de k=$baselineK à $baselineFreq MHz, le calcul ajuste le facteur k pour la bande actuelle de $frequencyMHz MHz, ce qui définit la limite incurvée de l\'horizon radio.';
}
@override
String get contacts_pathTrace => 'Traçage de chemin';
@@ -3290,90 +2891,4 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'meshcore-open exporter les données de carte GPX';
@override
String get snrIndicator_nearByRepeaters => 'Répéteurs à proximité';
@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 serveurs de salle';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Autoriser le compagnon à ajouter automatiquement les serveurs de salles 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 ?';
}
+15 -498
View File
@@ -38,9 +38,6 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get common_delete => 'Elimina';
@override
String get common_deleteAll => 'Elimina tutto';
@override
String get common_close => 'Chiudi';
@@ -111,137 +108,6 @@ 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...';
@@ -277,23 +143,6 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get scanner_scan => 'Scansiona';
@override
String get scanner_bluetoothOff => 'Il Bluetooth è disattivato.';
@override
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';
@override
String get device_quickSwitch => 'Passa velocemente';
@@ -374,13 +223,6 @@ 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';
@@ -470,10 +312,6 @@ class AppLocalizationsIt extends AppLocalizations {
String get settings_aboutDescription =>
'Un client Flutter open-source per i dispositivi di rete mesh LoRa Core di MeshCore.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Dati di elevazione LOS: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Nome';
@@ -498,6 +336,15 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get settings_presets => 'Preset';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frequenza (MHz)';
@@ -526,15 +373,10 @@ class AppLocalizationsIt extends AppLocalizations {
String get settings_txPowerInvalid => 'Potere TX non valido (0-22 dBm)';
@override
String get settings_clientRepeat => 'Ripetizione \"fuori dalla rete\"';
String get settings_longRange => 'Lungo Raggio';
@override
String get settings_clientRepeatSubtitle =>
'Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri.';
@override
String get settings_clientRepeatFreqWarning =>
'Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.';
String get settings_fastSpeed => 'Velocità Rapida';
@override
String settings_error(String message) {
@@ -610,14 +452,6 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get appSettings_languageUk => 'Ucraino';
@override
String get appSettings_enableMessageTracing =>
'Abilita tracciamento messaggi';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Mostra metadati dettagliati su instradamento e tempi per i messaggi';
@override
String get appSettings_notifications => 'Notifiche';
@@ -779,15 +613,6 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Cache Mappa Offline';
@override
String get appSettings_unitsTitle => 'Unità';
@override
String get appSettings_unitsMetric => 'Metrico (m/km)';
@override
String get appSettings_unitsImperial => 'Imperiale (ft / mi)';
@override
String get appSettings_noAreaSelected => 'Nessun\'area selezionata';
@@ -825,35 +650,7 @@ class AppLocalizationsIt extends AppLocalizations {
'I contatti appariranno quando i dispositivi pubblicizzano.';
@override
String get contacts_unread => 'Non letti';
@override
String get contacts_searchContactsNoNumber => 'Cerca Contatti...';
@override
String contacts_searchContacts(int number, String str) {
return 'Cerca contatti...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Cerca $number$str Preferiti...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Cerca $number$str Utenti...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Cerca $number$str Ripetitori...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Cerca $number$str server Room...';
}
String get contacts_searchContacts => 'Cerca contatti...';
@override
String get contacts_noUnreadContacts => 'Nessun contatto non letto';
@@ -978,12 +775,6 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get channels_editChannel => 'Modifica canale';
@override
String get channels_muteChannel => 'Silenzia canale';
@override
String get channels_unmuteChannel => 'Attiva notifiche canale';
@override
String get channels_deleteChannel => 'Elimina canale';
@@ -992,11 +783,6 @@ class AppLocalizationsIt extends AppLocalizations {
return 'Eliminare \"$name\"? Non può essere annullato.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Impossibile eliminare il canale \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Canale \"$name\" eliminato';
@@ -1285,9 +1071,6 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get chat_pathManagement => 'Gestione Percorsi';
@override
String get chat_ShowAllPaths => 'Mostra tutti i percorsi';
@override
String get chat_routingMode => 'Modalità di routing';
@@ -1447,12 +1230,6 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get map_title => 'Mappa Nodi';
@override
String get map_lineOfSight => 'Linea di vista';
@override
String get map_losScreenTitle => 'Linea di vista';
@override
String get map_noNodesWithLocation => 'Nessun nodo con dati di posizione';
@@ -1510,9 +1287,6 @@ 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';
@@ -1573,15 +1347,6 @@ 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';
@@ -1594,18 +1359,6 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get map_manageRepeater => 'Gestisci Ripetitore';
@override
String get map_tapToAdd => 'Tocca i nodi per aggiungerli al percorso.';
@override
String get map_runTrace => 'Esegui Path Trace';
@override
String get map_removeLast => 'Rimuovi ultimo';
@override
String get map_pathTraceCancelled => 'Tracciamento del percorso annullato.';
@override
String get mapCache_title => 'Cache Mappa Offline';
@@ -1901,10 +1654,10 @@ class AppLocalizationsIt extends AppLocalizations {
String get repeater_cliSubtitle => 'Invia comandi al ripetitore';
@override
String get repeater_neighbors => 'Vicini';
String get repeater_neighbours => 'Vicini';
@override
String get repeater_neighborsSubtitle =>
String get repeater_neighboursSubtitle =>
'Visualizza vicini di salto pari a zero.';
@override
@@ -2605,7 +2358,7 @@ class AppLocalizationsIt extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbors => 'Ripetitori Vicini';
String get neighbors_repeatersNeighbours => 'Ripetitori Vicini';
@override
String get neighbors_noData => 'Nessun dato sugli vicini disponibile.';
@@ -2914,15 +2667,6 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get listFilter_all => 'Tutti';
@override
String get listFilter_favorites => 'Preferiti';
@override
String get listFilter_addToFavorites => 'Aggiungi ai preferiti';
@override
String get listFilter_removeFromFavorites => 'Rimuovi dai preferiti';
@override
String get listFilter_users => 'Utenti';
@@ -2955,148 +2699,6 @@ class AppLocalizationsIt extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'Uno o più dei luppoli mancano di una posizione!';
@override
String get pathTrace_clearTooltip => 'Pulisci percorso';
@override
String get losSelectStartEnd =>
'Seleziona i nodi iniziali e finali per la LOS.';
@override
String losRunFailed(String error) {
return 'Controllo della linea di vista fallito: $error';
}
@override
String get losClearAllPoints => 'Cancella tutti i punti';
@override
String get losRunToViewElevationProfile =>
'Eseguire LOS per visualizzare il profilo altimetrico';
@override
String get losMenuTitle => 'Menù LOS';
@override
String get losMenuSubtitle =>
'Tocca i nodi o premi a lungo la mappa per punti personalizzati';
@override
String get losShowDisplayNodes => 'Mostra i nodi di visualizzazione';
@override
String get losCustomPoints => 'Punti personalizzati';
@override
String losCustomPointLabel(int index) {
return 'Personalizzato $index';
}
@override
String get losPointA => 'Punto A';
@override
String get losPointB => 'Punto B';
@override
String losAntennaA(String value, String unit) {
return 'Antenna A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antenna B: $value $unit';
}
@override
String get losRun => 'Esegui LOS';
@override
String get losNoElevationData => 'Nessun dato di elevazione';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, libera LOS, distanza minima $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, bloccato da $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: controllo...';
@override
String get losStatusNoData => 'LOS: nessun dato';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total libera, $blocked bloccato, $unknown sconosciuto';
}
@override
String get losErrorElevationUnavailable =>
'Dati di elevazione non disponibili per uno o più campioni.';
@override
String get losErrorInvalidInput =>
'Dati punti/elevazione non validi per il calcolo della LOS.';
@override
String get losRenameCustomPoint => 'Rinomina punto personalizzato';
@override
String get losPointName => 'Nome del punto';
@override
String get losShowPanelTooltip => 'Mostra il pannello LOS';
@override
String get losHidePanelTooltip => 'Nascondi il pannello LOS';
@override
String get losElevationAttribution =>
'Dati di elevazione: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Orizzonte radio';
@override
String get losLegendLosBeam => 'Linea di vista';
@override
String get losLegendTerrain => 'Terreno';
@override
String get losFrequencyLabel => 'Frequenza';
@override
String get losFrequencyInfoTooltip => 'Visualizza i dettagli del calcolo';
@override
String get losFrequencyDialogTitle => 'Calcolo dellorizzonte radio';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Partendo da k=$baselineK a $baselineFreq MHz, il calcolo regola il fattore k per l\'attuale banda $frequencyMHz MHz, che definisce il limite curvo dell\'orizzonte radio.';
}
@override
String get contacts_pathTrace => 'Traccia Percorso';
@@ -3270,89 +2872,4 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'meshcore-open esportazione dati mappa GPX';
@override
String get snrIndicator_nearByRepeaters => 'Ripetitori vicini';
@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?';
}
+15 -495
View File
@@ -38,9 +38,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get common_delete => 'Verwijderen';
@override
String get common_deleteAll => 'Alles verwijderen';
@override
String get common_close => 'Sluiten';
@@ -111,134 +108,6 @@ 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...';
@@ -273,23 +142,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get scanner_scan => 'Scan';
@override
String get scanner_bluetoothOff => 'Bluetooth is uitgeschakeld';
@override
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';
@override
String get device_quickSwitch => 'Snelle overschakeling';
@@ -371,13 +223,6 @@ 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';
@@ -465,10 +310,6 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_aboutDescription =>
'Een open-source Flutter client voor MeshCore LoRa mesh netwerkapparaten.';
@override
String get settings_aboutOpenMeteoAttribution =>
'LOS-hoogtegegevens: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Naam';
@@ -493,6 +334,15 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get settings_presets => 'Presets';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frequentie (MHz)';
@@ -521,15 +371,10 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_txPowerInvalid => 'Ongeldige TX-vermogen (0-22 dBm)';
@override
String get settings_clientRepeat => 'Herhalen: Afgekoppeld';
String get settings_longRange => 'Lange Afstand';
@override
String get settings_clientRepeatSubtitle =>
'Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.';
@override
String get settings_clientRepeatFreqWarning =>
'Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist.';
String get settings_fastSpeed => 'Hoge Snelheid';
@override
String settings_error(String message) {
@@ -605,13 +450,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get appSettings_languageUk => 'Oekraïens';
@override
String get appSettings_enableMessageTracing => 'Berichttracking inschakelen';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Gedetailleerde routerings- en timing-metadata voor berichten weergeven';
@override
String get appSettings_notifications => 'Notificaties';
@@ -773,15 +611,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Offline Kaarten Cache';
@override
String get appSettings_unitsTitle => 'Eenheden';
@override
String get appSettings_unitsMetric => 'Metrisch (m / km)';
@override
String get appSettings_unitsImperial => 'Imperiaal (ft / mi)';
@override
String get appSettings_noAreaSelected => 'Geen gebied geselecteerd';
@@ -819,35 +648,7 @@ class AppLocalizationsNl extends AppLocalizations {
'Contacten verschijnen wanneer apparaten zich aanbieden.';
@override
String get contacts_unread => 'Ongelezen';
@override
String get contacts_searchContactsNoNumber => 'Zoek contacten...';
@override
String contacts_searchContacts(int number, String str) {
return 'Zoek contacten...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Zoek $number$str favorieten...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Zoek $number$str gebruikers...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Zoek $number$str Repeaters...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Zoek $number$str Room servers...';
}
String get contacts_searchContacts => 'Zoek contacten...';
@override
String get contacts_noUnreadContacts => 'Geen ongelezen contacten';
@@ -972,12 +773,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get channels_editChannel => 'Kanaal bewerken';
@override
String get channels_muteChannel => 'Kanaal dempen';
@override
String get channels_unmuteChannel => 'Kanaal dempen opheffen';
@override
String get channels_deleteChannel => 'Kanaal verwijderen';
@@ -986,11 +781,6 @@ class AppLocalizationsNl extends AppLocalizations {
return 'Verwijderen \"$name\"? Dit kan niet worden teruggedraaid.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Kan kanaal $name niet verwijderen';
}
@override
String channels_channelDeleted(String name) {
return 'Kanaal \"$name\" verwijderd';
@@ -1278,9 +1068,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get chat_pathManagement => 'Beheer van Paden';
@override
String get chat_ShowAllPaths => 'Toon alle paden';
@override
String get chat_routingMode => 'Routeerwijze';
@@ -1439,12 +1226,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get map_title => 'Node Map';
@override
String get map_lineOfSight => 'Zichtlijn';
@override
String get map_losScreenTitle => 'Zichtlijn';
@override
String get map_noNodesWithLocation => 'Geen nodes met locatiegegevens';
@@ -1502,9 +1283,6 @@ 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';
@@ -1565,16 +1343,6 @@ 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';
@@ -1587,19 +1355,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get map_manageRepeater => 'Beheer Repeater';
@override
String get map_tapToAdd =>
'Tik op knooppunten om ze toe te voegen aan het pad';
@override
String get map_runTrace => 'Padeshulp traceren';
@override
String get map_removeLast => 'Verwijder Laatste';
@override
String get map_pathTraceCancelled => 'Pad traceren geannuleerd';
@override
String get mapCache_title => 'Offline Kaarten Cache';
@@ -1894,10 +1649,10 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_cliSubtitle => 'Verzend commando\'s naar de repeater';
@override
String get repeater_neighbors => 'Buren';
String get repeater_neighbours => 'Buren';
@override
String get repeater_neighborsSubtitle => 'Bekijk nul hops buren.';
String get repeater_neighboursSubtitle => 'Bekijk nul hops buren.';
@override
String get repeater_settings => 'Instellingen';
@@ -2593,7 +2348,7 @@ class AppLocalizationsNl extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbors => 'Herhalingen Buren';
String get neighbors_repeatersNeighbours => 'Herhalingen Buren';
@override
String get neighbors_noData => 'Geen gegevens van buren beschikbaar.';
@@ -2903,15 +2658,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get listFilter_all => 'Alles';
@override
String get listFilter_favorites => 'Favorieten';
@override
String get listFilter_addToFavorites => 'Toevoegen aan favorieten';
@override
String get listFilter_removeFromFavorites => 'Verwijderen uit favorieten';
@override
String get listFilter_users => 'Gebruikers';
@@ -2943,148 +2689,6 @@ class AppLocalizationsNl extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'Een of meer van de hops ontbreken een locatie!';
@override
String get pathTrace_clearTooltip => 'Weg wissen';
@override
String get losSelectStartEnd =>
'Selecteer begin- en eindknooppunten voor LOS.';
@override
String losRunFailed(String error) {
return 'Zichtlijncontrole mislukt: $error';
}
@override
String get losClearAllPoints => 'Wis alle punten';
@override
String get losRunToViewElevationProfile =>
'Voer LOS uit om het hoogteprofiel te bekijken';
@override
String get losMenuTitle => 'LOS-menu';
@override
String get losMenuSubtitle =>
'Tik op knooppunten of druk lang op de kaart voor aangepaste punten';
@override
String get losShowDisplayNodes => 'Toon weergaveknooppunten';
@override
String get losCustomPoints => 'Aangepaste punten';
@override
String losCustomPointLabel(int index) {
return 'Aangepast $index';
}
@override
String get losPointA => 'Punt A';
@override
String get losPointB => 'Punt B';
@override
String losAntennaA(String value, String unit) {
return 'Antenne A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antenne B: $value $unit';
}
@override
String get losRun => 'Voer LOS uit';
@override
String get losNoElevationData => 'Geen hoogtegegevens';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, vrije LOS, min. vrije ruimte $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, geblokkeerd door $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: controleren...';
@override
String get losStatusNoData => 'LOS: geen gegevens';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total gewist, $blocked geblokkeerd, $unknown onbekend';
}
@override
String get losErrorElevationUnavailable =>
'Hoogtegegevens niet beschikbaar voor een of meer monsters.';
@override
String get losErrorInvalidInput =>
'Ongeldige punten/hoogtegegevens voor LOS-berekening.';
@override
String get losRenameCustomPoint => 'Hernoem aangepast punt';
@override
String get losPointName => 'Puntnaam';
@override
String get losShowPanelTooltip => 'Toon LOS-paneel';
@override
String get losHidePanelTooltip => 'LOS-paneel verbergen';
@override
String get losElevationAttribution =>
'Hoogtegegevens: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Radiohorizon';
@override
String get losLegendLosBeam => 'Zichtlijn';
@override
String get losLegendTerrain => 'Terrein';
@override
String get losFrequencyLabel => 'Frequentie';
@override
String get losFrequencyInfoTooltip => 'Bekijk details van de berekening';
@override
String get losFrequencyDialogTitle => 'Berekening van de radiohorizon';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Beginnend met k=$baselineK bij $baselineFreq MHz, wordt bij de berekening de k-factor aangepast voor de huidige $frequencyMHz MHz-band, die de gebogen radiohorizonkap definieert.';
}
@override
String get contacts_pathTrace => 'Pad Traceren';
@@ -3255,88 +2859,4 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'meshcore-open GPX kaartgegevens exporteren';
@override
String get snrIndicator_nearByRepeaters => 'Nabije herhalingseenheden';
@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?';
}
+15 -497
View File
@@ -38,9 +38,6 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get common_delete => 'Usuń';
@override
String get common_deleteAll => 'Usuń wszystko';
@override
String get common_close => 'Zamknąć';
@@ -111,138 +108,6 @@ class AppLocalizationsPl 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 => 'Połącz się za pomocą protokołu TCP';
@override
String get tcpHostLabel => 'Adres IP';
@override
String get tcpHostHint => '192.168.40.10';
@override
String get tcpPortLabel => 'Port';
@override
String get tcpPortHint => '5000';
@override
String get tcpStatus_notConnected => 'Wprowadź adres URL i połącz';
@override
String tcpStatus_connectingTo(String endpoint) {
return 'Połączenie z $endpoint...';
}
@override
String get tcpErrorHostRequired => 'Wymagana jest adresa IP.';
@override
String get tcpErrorPortInvalid =>
'Numer portu musi mieścić się w zakresie od 1 do 65535.';
@override
String get tcpErrorUnsupported =>
'Transport protokoł TCP nie jest obsługiwany na tym urządzeniu.';
@override
String get tcpErrorTimedOut =>
'Połączenie TCP zakończyło się bez powodzenia.';
@override
String tcpConnectionFailed(String error) {
return 'Błąd połączenia TCP: $error';
}
@override
String get usbScreenTitle => 'Połącz przez USB';
@override
String get usbScreenSubtitle =>
'Wybierz wykryty urządzenie szeregowe i podłącz je bezpośrednio do swojego węzła MeshCore.';
@override
String get usbScreenStatus => 'Wybierz urządzenie USB';
@override
String get usbScreenNote =>
'Port szeregowy USB jest aktywny na urządzeniach z systemem Android i platformach stacjonarnych, które go obsługują.';
@override
String get usbScreenEmptyState =>
'Nie znaleziono żadnych urządzeń USB. Podłącz jedno i zaktualizuj.';
@override
String get usbErrorPermissionDenied =>
'Zostało odrzucone żądanie dostępu przez USB.';
@override
String get usbErrorDeviceMissing =>
'Wybór urządzenia USB już nie jest dostępny.';
@override
String get usbErrorInvalidPort => 'Wybierz prawidłowe urządzenie USB.';
@override
String get usbErrorBusy =>
'Kolejne żądanie połączenia przez USB jest już w trakcie realizacji.';
@override
String get usbErrorNotConnected => 'Brak podłączonego urządzenia USB.';
@override
String get usbErrorOpenFailed =>
'Nie udało się otworzyć wybranego urządzenia USB.';
@override
String get usbErrorConnectFailed =>
'Nie udało się połączyć z wybranym urządzeniem USB.';
@override
String get usbErrorUnsupported =>
'Port szeregowy USB nie jest obsługiwany na tym urządzeniu.';
@override
String get usbErrorAlreadyActive => 'Połączenie USB jest już aktywne.';
@override
String get usbErrorNoDeviceSelected =>
'Nie został wybrany żaden urządzenie USB.';
@override
String get usbErrorPortClosed => 'Połączenie USB nie jest aktywne.';
@override
String get usbErrorConnectTimedOut =>
'Połączenie nie zostało nawiązane. Upewnij się, że urządzenie posiada oprogramowanie \"USB Companion\".';
@override
String get usbFallbackDeviceName =>
'Urządzenie do komunikacji przez sieć (seria)';
@override
String get usbStatus_notConnected => 'Wybierz urządzenie USB';
@override
String get usbStatus_connecting => 'Połączenie z urządzeniem USB...';
@override
String get usbStatus_searching => 'Wyszukiwanie urządzeń USB...';
@override
String usbConnectionFailed(String error) {
return 'Błąd połączenia USB: $error';
}
@override
String get scanner_scanning => 'Skanowanie urządzeń...';
@@ -278,23 +143,6 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get scanner_scan => 'Przeskanuj';
@override
String get scanner_bluetoothOff => 'Bluetooth jest wyłączony';
@override
String get scanner_bluetoothOffMessage =>
'Prosimy włączyć Bluetooth, aby przeskanować urządzenia.';
@override
String get scanner_chromeRequired => 'Wymagana przeglądarka Chrome';
@override
String get scanner_chromeRequiredMessage =>
'Ta aplikacja internetowa wymaga przeglądarki Google Chrome lub opartej na Chromium do obsługi Bluetooth.';
@override
String get scanner_enableBluetooth => 'Włącz Bluetooth';
@override
String get device_quickSwitch => 'Szybka zmiana';
@@ -377,13 +225,6 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get settings_longitude => 'Długość';
@override
String get settings_contactSettings => 'Ustawienia kontaktowe';
@override
String get settings_contactSettingsSubtitle =>
'Ustawienia dotyczące sposobu dodawania kontaktów';
@override
String get settings_privacyMode => 'Tryb Prywatny';
@@ -472,10 +313,6 @@ class AppLocalizationsPl extends AppLocalizations {
String get settings_aboutDescription =>
'Otwarty kod źródłowy klient Flutter dla urządzeń do sieci mesh LoRa MeshCore.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Dane wysokościowe LOS: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Imię';
@@ -500,6 +337,15 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get settings_presets => 'Preset';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Częstotliwość (MHz)';
@@ -529,15 +375,10 @@ class AppLocalizationsPl extends AppLocalizations {
String get settings_txPowerInvalid => 'Nieprawidłowa moc TX (0-22 dBm)';
@override
String get settings_clientRepeat => 'Powtórzenie: Niezależne od sieci';
String get settings_longRange => 'Długi zasięg';
@override
String get settings_clientRepeatSubtitle =>
'Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.';
@override
String get settings_clientRepeatFreqWarning =>
'Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz.';
String get settings_fastSpeed => 'Szybka prędkość';
@override
String settings_error(String message) {
@@ -613,13 +454,6 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get appSettings_languageUk => 'Ukraińska';
@override
String get appSettings_enableMessageTracing => 'Włącz śledzenie wiadomości';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Pokaż szczegółowe metadane trasowania i czasu dla wiadomości';
@override
String get appSettings_notifications => 'Powiadomienia';
@@ -781,15 +615,6 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Bufor Map Offline';
@override
String get appSettings_unitsTitle => 'Jednostki';
@override
String get appSettings_unitsMetric => 'Metryczne (m / km)';
@override
String get appSettings_unitsImperial => 'Imperialne (ft / mi)';
@override
String get appSettings_noAreaSelected => 'Nie zaznaczono żadnej powierzchni.';
@@ -827,35 +652,7 @@ class AppLocalizationsPl extends AppLocalizations {
'Kontakty będą wyświetlane, gdy urządzenia reklamują się.';
@override
String get contacts_unread => 'Nieprzeczytane';
@override
String get contacts_searchContactsNoNumber => 'Wyszukaj kontakty...';
@override
String contacts_searchContacts(int number, String str) {
return 'Wyszukaj kontakty...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Wyszukaj $number$str ulubione...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Wyszukaj $number$str Użytkowników...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Wyszukaj $number$str powtórników...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Wyszukaj $number$str serwerów Room...';
}
String get contacts_searchContacts => 'Wyszukaj kontakty...';
@override
String get contacts_noUnreadContacts => 'Brak nieprzeczytanych kontaktów';
@@ -981,12 +778,6 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get channels_editChannel => 'Edytuj kanał';
@override
String get channels_muteChannel => 'Wycisz kanał';
@override
String get channels_unmuteChannel => 'Wyłącz wyciszenie kanału';
@override
String get channels_deleteChannel => 'Usuń kanał';
@@ -995,11 +786,6 @@ class AppLocalizationsPl extends AppLocalizations {
return 'Usuń \"$name\"? Nie można tego cofnąć.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Nie udało się usunąć kanału \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Kanał \"$name\" usunięto';
@@ -1287,9 +1073,6 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get chat_pathManagement => 'Zarządzanie ścieżkami';
@override
String get chat_ShowAllPaths => 'Pokaż wszystkie ścieżki';
@override
String get chat_routingMode => 'Tryb routingu';
@@ -1449,12 +1232,6 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get map_title => 'Mapa węzłów';
@override
String get map_lineOfSight => 'Linia wzroku';
@override
String get map_losScreenTitle => 'Linia wzroku';
@override
String get map_noNodesWithLocation => 'Brak węzłów z danymi lokalizacyjnymi';
@@ -1512,9 +1289,6 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get map_shareMarkerHere => 'Udostępnij znacznik tutaj';
@override
String get map_setAsMyLocation => 'Ustaw jako moje lokalizację';
@override
String get map_pinLabel => 'Oznacz etykietę';
@@ -1575,16 +1349,6 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Pokaż współdzielone znaki.';
@override
String get map_showGuessedLocations =>
'Wyświetl lokalizacje zgadanych węzłów';
@override
String get map_showDiscoveryContacts => 'Pokaż kontakty odkrywania';
@override
String get map_guessedLocation => 'Wydana lokalizacja';
@override
String get map_lastSeenTime => 'Ostatni raz widiany';
@@ -1597,18 +1361,6 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get map_manageRepeater => 'Zarządzaj Powtórzami';
@override
String get map_tapToAdd => 'Kliknij na węzły, aby dodać je do ścieżki.';
@override
String get map_runTrace => 'Uruchom ślad ścieżki';
@override
String get map_removeLast => 'Usuń ostatni';
@override
String get map_pathTraceCancelled => 'Śledzenie ścieżki anulowano.';
@override
String get mapCache_title => 'Bufor Map Offline';
@@ -1906,10 +1658,10 @@ class AppLocalizationsPl extends AppLocalizations {
String get repeater_cliSubtitle => 'Wyślij polecenia do powielacza';
@override
String get repeater_neighbors => 'Sąsiedzi';
String get repeater_neighbours => 'Sąsiedzi';
@override
String get repeater_neighborsSubtitle =>
String get repeater_neighboursSubtitle =>
'Wyświetl sąsiedztwo zerowych hopów.';
@override
@@ -2605,7 +2357,7 @@ class AppLocalizationsPl extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbors => 'Powtarzacze Sąsiedzi';
String get neighbors_repeatersNeighbours => 'Powtarzacze Sąsiedzi';
@override
String get neighbors_noData => 'Brak danych dotyczących sąsiadów.';
@@ -2914,15 +2666,6 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get listFilter_all => 'Wszystko';
@override
String get listFilter_favorites => 'Ulubione';
@override
String get listFilter_addToFavorites => 'Dodaj do ulubionych';
@override
String get listFilter_removeFromFavorites => 'Usuń z ulubionych';
@override
String get listFilter_users => 'Użytkownicy';
@@ -2954,147 +2697,6 @@ class AppLocalizationsPl extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'Jeden lub więcej z chmieli nie ma określonej lokalizacji!';
@override
String get pathTrace_clearTooltip => 'Wyczyść ścieżkę';
@override
String get losSelectStartEnd => 'Wybierz węzły początkowe i końcowe dla LOS.';
@override
String losRunFailed(String error) {
return 'Sprawdzenie pola widzenia nie powiodło się: $error';
}
@override
String get losClearAllPoints => 'Wyczyść wszystkie punkty';
@override
String get losRunToViewElevationProfile =>
'Uruchom LOS, aby wyświetlić profil wysokości';
@override
String get losMenuTitle => 'Menu LOS';
@override
String get losMenuSubtitle =>
'Stuknij węzły lub naciśnij i przytrzymaj mapę, aby uzyskać niestandardowe punkty';
@override
String get losShowDisplayNodes => 'Pokaż węzły wyświetlające';
@override
String get losCustomPoints => 'Punkty niestandardowe';
@override
String losCustomPointLabel(int index) {
return 'Niestandardowe $index';
}
@override
String get losPointA => 'Punkt A';
@override
String get losPointB => 'Punkt B';
@override
String losAntennaA(String value, String unit) {
return 'Antena A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antena B: $value $unit';
}
@override
String get losRun => 'Uruchom LOS-a';
@override
String get losNoElevationData => 'Brak danych o wysokości';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, czysty LOS, minimalny prześwit $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, zablokowane przez $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: sprawdzam...';
@override
String get losStatusNoData => 'LOS: brak danych';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total jasne, $blocked zablokowane, $unknown nieznane';
}
@override
String get losErrorElevationUnavailable =>
'Dane dotyczące wysokości są niedostępne dla jednej lub większej liczby próbek.';
@override
String get losErrorInvalidInput =>
'Nieprawidłowe dane punktów/wysokości do obliczenia LOS.';
@override
String get losRenameCustomPoint => 'Zmień nazwę punktu niestandardowego';
@override
String get losPointName => 'Nazwa punktu';
@override
String get losShowPanelTooltip => 'Pokaż panel LOS';
@override
String get losHidePanelTooltip => 'Ukryj panel LOS';
@override
String get losElevationAttribution =>
'Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Horyzont radiowy';
@override
String get losLegendLosBeam => 'Linia widoczności';
@override
String get losLegendTerrain => 'Teren';
@override
String get losFrequencyLabel => 'Częstotliwość';
@override
String get losFrequencyInfoTooltip => 'Zobacz szczegóły obliczenia';
@override
String get losFrequencyDialogTitle => 'Obliczanie horyzontu radiowego';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Zaczynając od k=$baselineK przy $baselineFreq MHz, obliczenia korygują współczynnik k dla bieżącego pasma $frequencyMHz MHz, które definiuje zakrzywiony limit horyzontu radiowego.';
}
@override
String get contacts_pathTrace => 'Śledzenie Ścieżek';
@@ -3272,88 +2874,4 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'Eksport danych mapy GPX meshcore-open';
@override
String get snrIndicator_nearByRepeaters => 'Nadajniki w pobliżu';
@override
String get snrIndicator_lastSeen => 'Ostatnio widziany';
@override
String get contactsSettings_title => 'Ustawienia kontaktów';
@override
String get contactsSettings_autoAddTitle => 'Automatyczne odnajdywanie';
@override
String get contactsSettings_otherTitle =>
'Inne ustawienia związane z kontaktami';
@override
String get contactsSettings_autoAddUsersTitle =>
'Automatycznie dodaj użytkowników';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Pozwól towarzyszowi automatycznie dodawać znalezione użytkowników.';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Automatyczne dodawanie powtarzalników';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Zezwól na automatyczne dodawanie odkrytych repeaterów przez towarzysza.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Automatycznie dodaj serwery pokojowe';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Zezwól towarzyszowi na automatyczne dodawanie znalezionych serwerów pokojowych.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Automatycznie dodaj czujniki';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Zezwól towarzyszowi na automatyczne dodawanie wykrytych czujników.';
@override
String get contactsSettings_overwriteOldestTitle => 'Nadpisz najstarszy';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Gdy lista kontaktów jest pełna, najstarszy nieulubiony kontakt zostanie zastąpiony.';
@override
String get discoveredContacts_Title => 'Odkryte Kontakty';
@override
String get discoveredContacts_noMatching => 'Brak pasujących kontaktów';
@override
String get discoveredContacts_searchHint => 'Wyszukaj odkryte kontakty';
@override
String get discoveredContacts_contactAdded => 'Kontakt dodany';
@override
String get discoveredContacts_addContact => 'Dodaj kontakt';
@override
String get discoveredContacts_copyContact => 'Kopiuj kontakt do schowka';
@override
String get discoveredContacts_deleteContact => 'Usuń kontakt';
@override
String get discoveredContacts_deleteContactAll =>
'Usuń wszystkie odkryte kontakty';
@override
String get discoveredContacts_deleteContactAllContent =>
'Czy na pewno chcesz usunąć wszystkie znalezione kontakty?';
}
+16 -498
View File
@@ -38,9 +38,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get common_delete => 'Excluir';
@override
String get common_deleteAll => 'Excluir Tudo';
@override
String get common_close => 'Fechar';
@@ -111,136 +108,6 @@ 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...';
@@ -276,23 +143,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get scanner_scan => 'Digitalizar';
@override
String get scanner_bluetoothOff => 'Bluetooth está desativado';
@override
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';
@override
String get device_quickSwitch => 'Mudar rapidamente';
@@ -374,13 +224,6 @@ 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';
@@ -471,10 +314,6 @@ class AppLocalizationsPt extends AppLocalizations {
String get settings_aboutDescription =>
'Um cliente Flutter de código aberto para dispositivos de rede mesh LoRa Core da MeshCore.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Dados de elevação LOS: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Nome';
@@ -499,6 +338,15 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get settings_presets => 'Presets';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frequência (MHz)';
@@ -527,15 +375,10 @@ class AppLocalizationsPt extends AppLocalizations {
String get settings_txPowerInvalid => 'Potência de TX inválida (0-22 dBm)';
@override
String get settings_clientRepeat => 'Repetição sem rede';
String get settings_longRange => 'Alcance Longo';
@override
String get settings_clientRepeatSubtitle =>
'Permita que este dispositivo repita pacotes de rede para outros dispositivos.';
@override
String get settings_clientRepeatFreqWarning =>
'A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.';
String get settings_fastSpeed => 'Velocidade Rápida';
@override
String settings_error(String message) {
@@ -611,14 +454,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get appSettings_languageUk => 'Ucraniano';
@override
String get appSettings_enableMessageTracing =>
'Ativar rastreamento de mensagens';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Mostrar metadados detalhados de roteamento e tempo para as mensagens';
@override
String get appSettings_notifications => 'Notificações';
@@ -779,15 +614,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Cache de Mapa Offline';
@override
String get appSettings_unitsTitle => 'Unidades';
@override
String get appSettings_unitsMetric => 'Métrico (m/km)';
@override
String get appSettings_unitsImperial => 'Imperial (ft/mi)';
@override
String get appSettings_noAreaSelected => 'Nenhuma área selecionada';
@@ -826,35 +652,7 @@ class AppLocalizationsPt extends AppLocalizations {
'Os contatos serão exibidos quando os dispositivos anunciarem.';
@override
String get contacts_unread => 'Não lido';
@override
String get contacts_searchContactsNoNumber => 'Pesquisar Contatos...';
@override
String contacts_searchContacts(int number, String str) {
return 'Pesquisar contatos...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Pesquisar $number$str Favoritos...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Pesquisar $number$str Usuários...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Pesquisar $number$str Repetidores...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Pesquisar $number$str servidores de sala...';
}
String get contacts_searchContacts => 'Pesquisar contatos...';
@override
String get contacts_noUnreadContacts => 'Sem contatos não lidos.';
@@ -980,12 +778,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get channels_editChannel => 'Editar canal';
@override
String get channels_muteChannel => 'Silenciar canal';
@override
String get channels_unmuteChannel => 'Ativar canal';
@override
String get channels_deleteChannel => 'Excluir canal';
@@ -994,11 +786,6 @@ class AppLocalizationsPt extends AppLocalizations {
return 'Excluir \"$name\"? Não pode ser desfeito.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Falha ao excluir o canal \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Canal \"$name\" excluído';
@@ -1286,9 +1073,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get chat_pathManagement => 'Gerenciamento de Caminhos';
@override
String get chat_ShowAllPaths => 'Mostrar todos os caminhos';
@override
String get chat_routingMode => 'Modo de roteamento';
@@ -1447,12 +1231,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get map_title => 'Mapa de Nós';
@override
String get map_lineOfSight => 'Linha de visão';
@override
String get map_losScreenTitle => 'Linha de visão';
@override
String get map_noNodesWithLocation =>
'Não existem nós com dados de localização.';
@@ -1511,9 +1289,6 @@ 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';
@@ -1574,16 +1349,6 @@ 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';
@@ -1596,18 +1361,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get map_manageRepeater => 'Gerenciar Repetidor';
@override
String get map_tapToAdd => 'Toque nos nós para adicioná-los ao caminho.';
@override
String get map_runTrace => 'Executar Traçado de Caminho';
@override
String get map_removeLast => 'Remover Último';
@override
String get map_pathTraceCancelled => 'Rastreamento de caminho cancelado.';
@override
String get mapCache_title => 'Cache de Mapa Offline';
@@ -1903,10 +1656,11 @@ class AppLocalizationsPt extends AppLocalizations {
String get repeater_cliSubtitle => 'Enviar comandos ao repetidor';
@override
String get repeater_neighbors => 'Vizinhos';
String get repeater_neighbours => 'Vizinhos';
@override
String get repeater_neighborsSubtitle => 'Visualizar vizinhos de salto zero.';
String get repeater_neighboursSubtitle =>
'Visualizar vizinhos de salto zero.';
@override
String get repeater_settings => 'Configurações';
@@ -2605,7 +2359,7 @@ class AppLocalizationsPt extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbors => 'Repetidores Vizinhos';
String get neighbors_repeatersNeighbours => 'Repetidores Vizinhos';
@override
String get neighbors_noData => 'Não estão disponíveis dados de vizinhos.';
@@ -2915,15 +2669,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get listFilter_all => 'Tudo';
@override
String get listFilter_favorites => 'Favoritos';
@override
String get listFilter_addToFavorites => 'Adicionar aos favoritos';
@override
String get listFilter_removeFromFavorites => 'Remover da lista de favoritos';
@override
String get listFilter_users => 'Usuários';
@@ -2955,147 +2700,6 @@ class AppLocalizationsPt extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'Um ou mais dos lúpulos estão sem localização!';
@override
String get pathTrace_clearTooltip => 'Limpar caminho';
@override
String get losSelectStartEnd => 'Selecione nós iniciais e finais para LOS.';
@override
String losRunFailed(String error) {
return 'Falha na verificação da linha de visão: $error';
}
@override
String get losClearAllPoints => 'Limpe todos os pontos';
@override
String get losRunToViewElevationProfile =>
'Execute o LOS para visualizar o perfil de elevação';
@override
String get losMenuTitle => 'Menu LOS';
@override
String get losMenuSubtitle =>
'Toque nos nós ou mantenha pressionado o mapa para obter pontos personalizados';
@override
String get losShowDisplayNodes => 'Mostrar nós de exibição';
@override
String get losCustomPoints => 'Pontos personalizados';
@override
String losCustomPointLabel(int index) {
return '$index personalizado';
}
@override
String get losPointA => 'Ponto A';
@override
String get losPointB => 'Ponto B';
@override
String losAntennaA(String value, String unit) {
return 'Antena A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antena B: $value $unit';
}
@override
String get losRun => 'Executar LOS';
@override
String get losNoElevationData => 'Sem dados de elevação';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, limpar LOS, liberação mínima $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, bloqueado por $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: verificando...';
@override
String get losStatusNoData => 'LOS: sem dados';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total limpo, $blocked bloqueado, $unknown desconhecido';
}
@override
String get losErrorElevationUnavailable =>
'Dados de elevação indisponíveis para uma ou mais amostras.';
@override
String get losErrorInvalidInput =>
'Dados de pontos/elevação inválidos para cálculo de LOS.';
@override
String get losRenameCustomPoint => 'Renomear ponto personalizado';
@override
String get losPointName => 'Nome do ponto';
@override
String get losShowPanelTooltip => 'Mostrar painel LOS';
@override
String get losHidePanelTooltip => 'Ocultar painel LOS';
@override
String get losElevationAttribution =>
'Dados de elevação: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Horizonte de rádio';
@override
String get losLegendLosBeam => 'Linha de visada';
@override
String get losLegendTerrain => 'Terreno';
@override
String get losFrequencyLabel => 'Frequência';
@override
String get losFrequencyInfoTooltip => 'Ver detalhes do cálculo';
@override
String get losFrequencyDialogTitle => 'Cálculo do horizonte de rádio';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Começando em k=$baselineK em $baselineFreq MHz, o cálculo ajusta o fator k para a banda atual de $frequencyMHz MHz, que define o limite do horizonte de rádio curvo.';
}
@override
String get contacts_pathTrace => 'Traçado de Caminho';
@@ -3265,90 +2869,4 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'meshcore-open exportação de dados de mapa GPX';
@override
String get snrIndicator_nearByRepeaters => 'Repetidores Próximos';
@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?';
}
+15 -499
View File
@@ -38,9 +38,6 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get common_delete => 'Удалить';
@override
String get common_deleteAll => 'Удалить все';
@override
String get common_close => 'Закрыть';
@@ -111,137 +108,6 @@ 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 => 'Поиск устройств...';
@@ -276,23 +142,6 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get scanner_scan => 'Сканирование';
@override
String get scanner_bluetoothOff => 'Bluetooth выключен';
@override
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';
@override
String get device_quickSwitch => 'Быстрое переключение';
@@ -373,13 +222,6 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get settings_longitude => 'Долгота';
@override
String get settings_contactSettings => 'Настройки контактов';
@override
String get settings_contactSettingsSubtitle =>
'Настройки добавления контактов';
@override
String get settings_privacyMode => 'Режим конфиденциальности';
@@ -469,10 +311,6 @@ class AppLocalizationsRu extends AppLocalizations {
String get settings_aboutDescription =>
'Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Данные о высоте LOS: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Имя';
@@ -497,6 +335,15 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get settings_presets => 'Пресеты';
@override
String get settings_preset915Mhz => '915 МГц';
@override
String get settings_preset868Mhz => '868 МГц';
@override
String get settings_preset433Mhz => '433 МГц';
@override
String get settings_frequency => 'Частота (МГц)';
@@ -526,15 +373,10 @@ class AppLocalizationsRu extends AppLocalizations {
'Недопустимая мощность передачи (0–22 дБм)';
@override
String get settings_clientRepeat => 'Повторение \"вне сети\"';
String get settings_longRange => 'Дальний радиус';
@override
String get settings_clientRepeatSubtitle =>
'Позвольте этому устройству повторять пакеты данных для других устройств.';
@override
String get settings_clientRepeatFreqWarning =>
'Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.';
String get settings_fastSpeed => 'Высокая скорость';
@override
String settings_error(String message) {
@@ -610,14 +452,6 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get appSettings_languageUk => 'Українська';
@override
String get appSettings_enableMessageTracing =>
'Включить трассировку сообщений';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Показывать подробные метаданные о маршрутизации и времени для сообщений';
@override
String get appSettings_notifications => 'Уведомления';
@@ -780,15 +614,6 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Кэш офлайн-карты';
@override
String get appSettings_unitsTitle => 'Единицы';
@override
String get appSettings_unitsMetric => 'Метрическая (м/км)';
@override
String get appSettings_unitsImperial => 'Имперская (ft / mi)';
@override
String get appSettings_noAreaSelected => 'Область не выбрана';
@@ -826,35 +651,7 @@ class AppLocalizationsRu extends AppLocalizations {
'Контакты появятся, когда устройства начнут рассылать оповещения';
@override
String get contacts_unread => 'Непрочитанное';
@override
String get contacts_searchContactsNoNumber => 'Поиск контактов...';
@override
String contacts_searchContacts(int number, String str) {
return 'Поиск контактов...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Поиск $number$str избранного...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Поиск $number$str пользователей...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Поиск $number$str ретрансляторов...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Поиск $number$str серверов комнат...';
}
String get contacts_searchContacts => 'Поиск контактов...';
@override
String get contacts_noUnreadContacts => 'Нет непрочитанных контактов';
@@ -979,12 +776,6 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get channels_editChannel => 'Изменить канал';
@override
String get channels_muteChannel => 'Отключить уведомления канала';
@override
String get channels_unmuteChannel => 'Включить уведомления канала';
@override
String get channels_deleteChannel => 'Удалить канал';
@@ -993,11 +784,6 @@ class AppLocalizationsRu extends AppLocalizations {
return 'Удалить \"$name\"? Это действие нельзя отменить.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Не удалось удалить канал $name.';
}
@override
String channels_channelDeleted(String name) {
return 'Канал \"$name\" удалён';
@@ -1285,9 +1071,6 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get chat_pathManagement => 'Управление маршрутами';
@override
String get chat_ShowAllPaths => 'Показать все пути';
@override
String get chat_routingMode => 'Режим маршрутизации';
@@ -1450,12 +1233,6 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get map_title => 'Карта нод';
@override
String get map_lineOfSight => 'Линия видимости';
@override
String get map_losScreenTitle => 'Линия видимости';
@override
String get map_noNodesWithLocation => 'Нет нод с данными о местоположении';
@@ -1513,9 +1290,6 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get map_shareMarkerHere => 'Поделиться меткой здесь';
@override
String get map_setAsMyLocation => 'Установить мое местоположение';
@override
String get map_pinLabel => 'Метка';
@@ -1576,16 +1350,6 @@ 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 => 'Время последнего появления';
@@ -1598,18 +1362,6 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get map_manageRepeater => 'Управление репитером';
@override
String get map_tapToAdd => 'Нажимайте на узлы, чтобы добавить их в путь.';
@override
String get map_runTrace => 'Запустить трассировку пути';
@override
String get map_removeLast => 'Удалить последний';
@override
String get map_pathTraceCancelled => 'Отмена трассировки пути';
@override
String get mapCache_title => 'Кэш офлайн-карты';
@@ -1906,10 +1658,10 @@ class AppLocalizationsRu extends AppLocalizations {
String get repeater_cliSubtitle => 'Отправка команд репитеру';
@override
String get repeater_neighbors => 'Соседи';
String get repeater_neighbours => 'Соседи';
@override
String get repeater_neighborsSubtitle => 'Просмотр соседей на нулевом хопе.';
String get repeater_neighboursSubtitle => 'Просмотр соседей на нулевом хопе.';
@override
String get repeater_settings => 'Настройки';
@@ -2609,7 +2361,7 @@ class AppLocalizationsRu extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbors => 'Соседи репитеров';
String get neighbors_repeatersNeighbours => 'Соседи репитеров';
@override
String get neighbors_noData => 'Данные о соседях недоступны.';
@@ -2919,15 +2671,6 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get listFilter_all => 'Все';
@override
String get listFilter_favorites => 'Избранное';
@override
String get listFilter_addToFavorites => 'Добавить в избранное';
@override
String get listFilter_removeFromFavorites => 'Удалить из избранного';
@override
String get listFilter_users => 'Пользователи';
@@ -2959,147 +2702,6 @@ class AppLocalizationsRu extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'Одному или нескольким хмелям не указано местоположение!';
@override
String get pathTrace_clearTooltip => 'Очистить путь';
@override
String get losSelectStartEnd => 'Выберите начальный и конечный узлы для LOS.';
@override
String losRunFailed(String error) {
return 'Проверка прямой видимости не удалась: $error';
}
@override
String get losClearAllPoints => 'Очистить все точки';
@override
String get losRunToViewElevationProfile =>
'Запустите LOS, чтобы просмотреть профиль высот.';
@override
String get losMenuTitle => 'ЛОС Меню';
@override
String get losMenuSubtitle =>
'Коснитесь узлов или нажмите и удерживайте карту для выбора пользовательских точек.';
@override
String get losShowDisplayNodes => 'Показать узлы отображения';
@override
String get losCustomPoints => 'Пользовательские точки';
@override
String losCustomPointLabel(int index) {
return 'Пользовательский $index';
}
@override
String get losPointA => 'Точка А';
@override
String get losPointB => 'Точка Б';
@override
String losAntennaA(String value, String unit) {
return 'Антенна А: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Антенна Б: $value $unit';
}
@override
String get losRun => 'Запустить ЛОС';
@override
String get losNoElevationData => 'Нет данных о высоте';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, свободная зона видимости, минимальный зазор $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, заблокирован $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'ЛОС: проверяю...';
@override
String get losStatusNoData => 'ЛОС: нет данных';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total очищено, $blocked заблокировано, $unknown неизвестно.';
}
@override
String get losErrorElevationUnavailable =>
'Данные о высоте недоступны для одного или нескольких образцов.';
@override
String get losErrorInvalidInput =>
'Неверные данные о точках/высоте для расчета LOS.';
@override
String get losRenameCustomPoint => 'Переименовать пользовательскую точку';
@override
String get losPointName => 'Имя точки';
@override
String get losShowPanelTooltip => 'Показать панель LOS';
@override
String get losHidePanelTooltip => 'Скрыть панель LOS';
@override
String get losElevationAttribution =>
'Данные о высоте: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Радиогоризонт';
@override
String get losLegendLosBeam => 'Линия прямой видимости';
@override
String get losLegendTerrain => 'Рельеф';
@override
String get losFrequencyLabel => 'Частота';
@override
String get losFrequencyInfoTooltip => 'Просмотреть детали расчёта';
@override
String get losFrequencyDialogTitle => 'Расчёт радиогоризонта';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Начиная с k=$baselineK на частоте $baselineFreq МГц, расчет корректирует коэффициент k для текущего диапазона $frequencyMHz МГц, который определяет изогнутую границу радиогоризонта.';
}
@override
String get contacts_pathTrace => 'Трассировка пути';
@@ -3278,90 +2880,4 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'meshcore-open экспорт данных карты GPX';
@override
String get snrIndicator_nearByRepeaters => 'Ближайшие ретрансляторы';
@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 =>
'Вы уверены, что хотите удалить все обнаруженные контакты?';
}
+15 -494
View File
@@ -38,9 +38,6 @@ 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ť';
@@ -111,135 +108,6 @@ 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í...';
@@ -275,23 +143,6 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get scanner_scan => 'Skončiť';
@override
String get scanner_bluetoothOff => 'Bluetooth je vypnutý';
@override
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';
@override
String get device_quickSwitch => 'Rýchle prepínač';
@@ -372,13 +223,6 @@ 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';
@@ -466,10 +310,6 @@ class AppLocalizationsSk extends AppLocalizations {
String get settings_aboutDescription =>
'Otvorený zdrojový Flutter klient pre MeshCore LoRa sieťové zariadenia.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Údaje o nadmorskej výške LOS: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Meno';
@@ -494,6 +334,15 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get settings_presets => 'Prednastavenia';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frekvencia (MHz)';
@@ -522,15 +371,10 @@ class AppLocalizationsSk extends AppLocalizations {
String get settings_txPowerInvalid => 'Neplatná hodnota výkonu TX (0-22 dBm)';
@override
String get settings_clientRepeat => 'Opätovné použitie bez elektrickej siete';
String get settings_longRange => 'Dlhý dosah';
@override
String get settings_clientRepeatSubtitle =>
'Umožnite, aby toto zariadenie opakovávalo siete pre ostatných.';
@override
String get settings_clientRepeatFreqWarning =>
'Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.';
String get settings_fastSpeed => 'Rýchla rýchlosť';
@override
String settings_error(String message) {
@@ -606,13 +450,6 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get appSettings_languageUk => 'Ukrajinská';
@override
String get appSettings_enableMessageTracing => 'Povoliť sledovanie správ';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Zobraziť podrobné metadáta o smerovaní a časovaní správ';
@override
String get appSettings_notifications => 'Upozornenia';
@@ -771,15 +608,6 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Offline Mapa Pamäť';
@override
String get appSettings_unitsTitle => 'Jednotky';
@override
String get appSettings_unitsMetric => 'Metrické (m / km)';
@override
String get appSettings_unitsImperial => 'Imperiálne (ft / mi)';
@override
String get appSettings_noAreaSelected => 'Neoznačila sa žiadna oblasť';
@@ -817,35 +645,7 @@ class AppLocalizationsSk extends AppLocalizations {
'Kontakty sa zobrazia, keď zariadenia spúšťajú reklamu.';
@override
String get contacts_unread => 'Neprečítané';
@override
String get contacts_searchContactsNoNumber => 'Hľadať kontakty...';
@override
String contacts_searchContacts(int number, String str) {
return 'Vyhľadávajte kontakty...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Hľadať $number$str obľúbené...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Hľadať $number$str používateľov...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Hľadať $number$str opakovače...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Hľadaj $number$str serverov miestností...';
}
String get contacts_searchContacts => 'Vyhľadávajte kontakty...';
@override
String get contacts_noUnreadContacts => 'Žiadne neprečítané kontakty';
@@ -973,12 +773,6 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get channels_editChannel => 'Upraviť kanál';
@override
String get channels_muteChannel => 'Stlmiť kanál';
@override
String get channels_unmuteChannel => 'Zrušiť stlmenie kanála';
@override
String get channels_deleteChannel => 'Odstrániť kanál';
@@ -987,11 +781,6 @@ class AppLocalizationsSk extends AppLocalizations {
return 'Odstrániť \"$name\"? To sa nedá zrušiť.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Kanál \"$name\" sa nepodarilo odstrániť';
}
@override
String channels_channelDeleted(String name) {
return 'Kanál \"$name\" bol odstránený';
@@ -1279,9 +1068,6 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get chat_pathManagement => 'Správa ciest';
@override
String get chat_ShowAllPaths => 'Zobraziť všetky cesty';
@override
String get chat_routingMode => 'Režim trasy';
@@ -1441,12 +1227,6 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get map_title => 'Mapa uzlov';
@override
String get map_lineOfSight => 'Line of Sight';
@override
String get map_losScreenTitle => 'Line of Sight';
@override
String get map_noNodesWithLocation => 'Žiadne uzly s údajmi o polohe';
@@ -1504,9 +1284,6 @@ 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';
@@ -1567,16 +1344,6 @@ 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';
@@ -1589,18 +1356,6 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get map_manageRepeater => 'Spravovať Opakovanie';
@override
String get map_tapToAdd => 'Kliknite na uzly, aby ste ich pridali k ceste.';
@override
String get map_runTrace => 'Spustiť trasovaním cesty';
@override
String get map_removeLast => 'Odstrániť posledný';
@override
String get map_pathTraceCancelled => 'Zrušenie stopáže cesty bolo zrušené.';
@override
String get mapCache_title => 'Offline Mapa Pamäť';
@@ -1896,10 +1651,10 @@ class AppLocalizationsSk extends AppLocalizations {
String get repeater_cliSubtitle => 'Pošlite príkazy opakovaču';
@override
String get repeater_neighbors => 'Súsezný';
String get repeater_neighbours => 'Súsezný';
@override
String get repeater_neighborsSubtitle => 'Zobraziť susedné body bez skokov.';
String get repeater_neighboursSubtitle => 'Zobraziť susedné body bez skokov.';
@override
String get repeater_settings => 'Nastavenia';
@@ -2590,7 +2345,7 @@ class AppLocalizationsSk extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbors => 'Opakovadlá Súsezná';
String get neighbors_repeatersNeighbours => 'Opakovadlá Súsezná';
@override
String get neighbors_noData =>
@@ -2899,15 +2654,6 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get listFilter_all => 'Všetko';
@override
String get listFilter_favorites => 'Obľúbené';
@override
String get listFilter_addToFavorites => 'Pridaj do obľúbených';
@override
String get listFilter_removeFromFavorites => 'Odstrániť z označení';
@override
String get listFilter_users => 'Používatelia';
@@ -2939,147 +2685,6 @@ class AppLocalizationsSk extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'Jedna alebo viac chmeľov chýba lokalita!';
@override
String get pathTrace_clearTooltip => 'Zmazať cestu';
@override
String get losSelectStartEnd => 'Vyberte počiatočný a koncový uzol pre LOS.';
@override
String losRunFailed(String error) {
return 'Kontrola priamej viditeľnosti zlyhala: $error';
}
@override
String get losClearAllPoints => 'Vymazať všetky body';
@override
String get losRunToViewElevationProfile =>
'Ak chcete zobraziť výškový profil, spustite LOS';
@override
String get losMenuTitle => 'Menu LOS';
@override
String get losMenuSubtitle =>
'Klepnutím na uzly alebo dlhým stlačením mapy získate vlastné body';
@override
String get losShowDisplayNodes => 'Zobraziť uzly zobrazenia';
@override
String get losCustomPoints => 'Vlastné body';
@override
String losCustomPointLabel(int index) {
return 'Vlastné $index';
}
@override
String get losPointA => 'Bod A';
@override
String get losPointB => 'Bod B';
@override
String losAntennaA(String value, String unit) {
return 'Anténa A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Anténa B: $value $unit';
}
@override
String get losRun => 'Spustite LOS';
@override
String get losNoElevationData => 'Žiadne údaje o nadmorskej výške';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, vymazať LOS, min. vôľa $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, blokovaný $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: kontrolujem...';
@override
String get losStatusNoData => 'LOS: žiadne údaje';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total vymazané, $blocked blokované, $unknown neznáme';
}
@override
String get losErrorElevationUnavailable =>
'Údaje o nadmorskej výške nie sú k dispozícii pre jednu alebo viacero vzoriek.';
@override
String get losErrorInvalidInput =>
'Neplatné body/údaje o nadmorskej výške pre výpočet LOS.';
@override
String get losRenameCustomPoint => 'Premenovať vlastný bod';
@override
String get losPointName => 'Názov bodu';
@override
String get losShowPanelTooltip => 'Zobraziť panel LOS';
@override
String get losHidePanelTooltip => 'Skryť panel LOS';
@override
String get losElevationAttribution =>
'Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Rádiový horizont';
@override
String get losLegendLosBeam => 'Priama viditeľnosť';
@override
String get losLegendTerrain => 'Terén';
@override
String get losFrequencyLabel => 'Frekvencia';
@override
String get losFrequencyInfoTooltip => 'Zobraziť podrobnosti výpočtu';
@override
String get losFrequencyDialogTitle => 'Výpočet rádiového horizontu';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Počnúc od k=$baselineK pri $baselineFreq MHz výpočet upraví k-faktor pre aktuálne pásmo $frequencyMHz MHz, ktorý definuje zakrivený strop rádiového horizontu.';
}
@override
String get contacts_pathTrace => 'Sledovanie lúčov';
@@ -3251,88 +2856,4 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'meshcore-open export dát GPX mapových údajov';
@override
String get snrIndicator_nearByRepeaters => 'Miestne opakovače';
@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?';
}
+15 -490
View File
@@ -38,9 +38,6 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get common_delete => 'Izbrisati';
@override
String get common_deleteAll => 'Izbriši vse';
@override
String get common_close => 'Zapri';
@@ -111,133 +108,6 @@ 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...';
@@ -273,23 +143,6 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get scanner_scan => 'Skeniraj';
@override
String get scanner_bluetoothOff => 'Bluetooth je izklopljen';
@override
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';
@override
String get device_quickSwitch => 'Hitro preklop';
@@ -370,13 +223,6 @@ 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';
@@ -463,10 +309,6 @@ class AppLocalizationsSl extends AppLocalizations {
String get settings_aboutDescription =>
'Odprtokodni Flutter klient za naprave za LoRa omrežje MeshCore.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Podatki o višini LOS: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Ime';
@@ -491,6 +333,15 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get settings_presets => 'Prednastavitve';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frekvenca (MHz)';
@@ -519,15 +370,10 @@ class AppLocalizationsSl extends AppLocalizations {
String get settings_txPowerInvalid => 'Neveljavna TX moč (0-22 dBm)';
@override
String get settings_clientRepeat => 'Neovadno ponavljanje';
String get settings_longRange => 'DDolg doseg';
@override
String get settings_clientRepeatSubtitle =>
'Omogočite temu naprave, da ponavlja paketne sporočila za druge.';
@override
String get settings_clientRepeatFreqWarning =>
'Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.';
String get settings_fastSpeed => 'Visoka hitrost';
@override
String settings_error(String message) {
@@ -603,13 +449,6 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get appSettings_languageUk => 'Ukrajinsko';
@override
String get appSettings_enableMessageTracing => 'Omogoči sledenje sporočilom';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil';
@override
String get appSettings_notifications => 'Obvestila';
@@ -770,15 +609,6 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Shramba zemljevidov brez povezave';
@override
String get appSettings_unitsTitle => 'Enote';
@override
String get appSettings_unitsMetric => 'Metrična (m/km)';
@override
String get appSettings_unitsImperial => 'Imperialno (ft / mi)';
@override
String get appSettings_noAreaSelected => 'Območje ni izbrano';
@@ -816,35 +646,7 @@ class AppLocalizationsSl extends AppLocalizations {
'Stiki se bodo prikazali, ko se naprave oglasijo.';
@override
String get contacts_unread => 'Neprebrano';
@override
String get contacts_searchContactsNoNumber => 'Iskanje stikov...';
@override
String contacts_searchContacts(int number, String str) {
return 'Iskanje stikov...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Iskanje $number$str priljubljenih...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Išči $number$str uporabnikov...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Išči $number$str ponavljalnike...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Išči $number$str strežnikov sob...';
}
String get contacts_searchContacts => 'Iskanje stikov...';
@override
String get contacts_noUnreadContacts => 'Ne prebrani stiki.';
@@ -969,12 +771,6 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get channels_editChannel => 'Uredi kanal';
@override
String get channels_muteChannel => 'Utišaj kanal';
@override
String get channels_unmuteChannel => 'Vklopi obvestila kanala';
@override
String get channels_deleteChannel => 'Pošlji kanal';
@@ -983,11 +779,6 @@ class AppLocalizationsSl extends AppLocalizations {
return 'Izbrišem \"$name\"? To se ne da povrniti.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Kanala $name ni bilo mogoče izbrisati';
}
@override
String channels_channelDeleted(String name) {
return 'Kanal \"$name\" izbrisan.';
@@ -1275,9 +1066,6 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get chat_pathManagement => 'Upravljanje poti';
@override
String get chat_ShowAllPaths => 'Prikaži vse poti';
@override
String get chat_routingMode => 'Navodilo za usmerjevalni način';
@@ -1434,12 +1222,6 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get map_title => 'Mapa omrežja';
@override
String get map_lineOfSight => 'Linija vida';
@override
String get map_losScreenTitle => 'Linija vida';
@override
String get map_noNodesWithLocation =>
'Nihče od notranjih elementov nima podatkov o lokaciji.';
@@ -1498,9 +1280,6 @@ 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';
@@ -1561,15 +1340,6 @@ 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';
@@ -1582,18 +1352,6 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get map_manageRepeater => 'Upravljajte Ponovitve';
@override
String get map_tapToAdd => 'Pritisnite na vozlišča, da jih dodate poti.';
@override
String get map_runTrace => 'Zaženi sledenje poti';
@override
String get map_removeLast => 'Odstrani Zadnji';
@override
String get map_pathTraceCancelled => 'Spremljanje poti je prekinjeno.';
@override
String get mapCache_title =>
'Omrezni predpomnilnik zemljeških zemljejevskih slik';
@@ -1892,10 +1650,10 @@ class AppLocalizationsSl extends AppLocalizations {
'Pošlji ukazne povelje na ponovitveno enoto.';
@override
String get repeater_neighbors => 'Sosedi';
String get repeater_neighbours => 'Sosedi';
@override
String get repeater_neighborsSubtitle => 'Pogledati nič sosednjih hopjev.';
String get repeater_neighboursSubtitle => 'Pogledati nič sosednjih hopjev.';
@override
String get repeater_settings => 'Nastavitve';
@@ -2591,7 +2349,7 @@ class AppLocalizationsSl extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbors => 'Ponovitve Sosedi';
String get neighbors_repeatersNeighbours => 'Ponovitve Sosedi';
@override
String get neighbors_noData => 'Niso na voljo podatki o sosedih.';
@@ -2899,15 +2657,6 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get listFilter_all => 'Vse';
@override
String get listFilter_favorites => 'Priljubljene';
@override
String get listFilter_addToFavorites => 'Dodaj v priljubljene';
@override
String get listFilter_removeFromFavorites => 'Odstrani iz priljubljenih';
@override
String get listFilter_users => 'Uporabniki';
@@ -2939,147 +2688,6 @@ class AppLocalizationsSl extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'Ena ali več hmelju manjka lokacija!';
@override
String get pathTrace_clearTooltip => 'Počisti pot';
@override
String get losSelectStartEnd => 'Izberite začetno in končno vozlišče za LOS.';
@override
String losRunFailed(String error) {
return 'Preverjanje vidnega polja ni uspelo: $error';
}
@override
String get losClearAllPoints => 'Počisti vse točke';
@override
String get losRunToViewElevationProfile =>
'Zaženite LOS za ogled višinskega profila';
@override
String get losMenuTitle => 'LOS meni';
@override
String get losMenuSubtitle =>
'Tapnite vozlišča ali dolgo pritisnite na zemljevid za točke po meri';
@override
String get losShowDisplayNodes => 'Pokaži prikazna vozlišča';
@override
String get losCustomPoints => 'Točke po meri';
@override
String losCustomPointLabel(int index) {
return 'Po meri $index';
}
@override
String get losPointA => 'Točka A';
@override
String get losPointB => 'Točka B';
@override
String losAntennaA(String value, String unit) {
return 'Antena A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antena B: $value $unit';
}
@override
String get losRun => 'Zaženi LOS';
@override
String get losNoElevationData => 'Ni podatkov o višini';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, čisti LOS, najmanjša razdalja $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, blokiral $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: preverjam ...';
@override
String get losStatusNoData => 'LOS: ni podatkov';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total jasno, $blocked blokirano, $unknown neznano';
}
@override
String get losErrorElevationUnavailable =>
'Podatki o nadmorski višini niso na voljo za enega ali več vzorcev.';
@override
String get losErrorInvalidInput =>
'Neveljavni podatki o točkah/višini za izračun LOS.';
@override
String get losRenameCustomPoint => 'Preimenujte točko po meri';
@override
String get losPointName => 'Ime točke';
@override
String get losShowPanelTooltip => 'Pokaži ploščo LOS';
@override
String get losHidePanelTooltip => 'Skrij ploščo LOS';
@override
String get losElevationAttribution =>
'Podatki o višini: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Radijski horizont';
@override
String get losLegendLosBeam => 'Linija vidnosti';
@override
String get losLegendTerrain => 'Teren';
@override
String get losFrequencyLabel => 'Frekvenca';
@override
String get losFrequencyInfoTooltip => 'Prikaži podrobnosti izračuna';
@override
String get losFrequencyDialogTitle => 'Izračun radijskega horizonta';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Začenši od k=$baselineK pri $baselineFreq MHz, izračun prilagodi k-faktor za trenutni pas $frequencyMHz MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.';
}
@override
String get contacts_pathTrace => 'Sledenje poti';
@@ -3253,87 +2861,4 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'meshcore-open izvoz podatkov GPX karte';
@override
String get snrIndicator_nearByRepeaters => 'Bližnji ponovitelji';
@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?';
}
+15 -490
View File
@@ -38,9 +38,6 @@ 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';
@@ -111,133 +108,6 @@ 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...';
@@ -272,23 +142,6 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get scanner_scan => 'Skanna';
@override
String get scanner_bluetoothOff => 'Bluetooth är avstängt';
@override
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';
@override
String get device_quickSwitch => 'Snabb växling';
@@ -369,13 +222,6 @@ 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';
@@ -461,10 +307,6 @@ class AppLocalizationsSv extends AppLocalizations {
String get settings_aboutDescription =>
'En öppen källkods Flutter-klient för MeshCore LoRa meshnätverksenheter.';
@override
String get settings_aboutOpenMeteoAttribution =>
'LOS-höjddata: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Namn';
@@ -489,6 +331,15 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get settings_presets => 'Fördefinierade inställningar';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frekvens (MHz)';
@@ -517,15 +368,10 @@ class AppLocalizationsSv extends AppLocalizations {
String get settings_txPowerInvalid => 'Ogiltig TX-effekt (0-22 dBm)';
@override
String get settings_clientRepeat => 'Upprepa utan elnät';
String get settings_longRange => 'Lång räckvidd';
@override
String get settings_clientRepeatSubtitle =>
'Låt enheten repetera nätpaket för andra användare.';
@override
String get settings_clientRepeatFreqWarning =>
'För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz.';
String get settings_fastSpeed => 'Snabb hastighet';
@override
String settings_error(String message) {
@@ -601,13 +447,6 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get appSettings_languageUk => 'Ukrainska';
@override
String get appSettings_enableMessageTracing => 'Aktivera meddelandespårning';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden';
@override
String get appSettings_notifications => 'Meddelanden';
@@ -765,15 +604,6 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Offline Kartcache';
@override
String get appSettings_unitsTitle => 'Enheter';
@override
String get appSettings_unitsMetric => 'Metriskt (m/km)';
@override
String get appSettings_unitsImperial => 'Imperialt (ft / mi)';
@override
String get appSettings_noAreaSelected => 'Ingen area markerad';
@@ -811,35 +641,7 @@ class AppLocalizationsSv extends AppLocalizations {
'Kontakter kommer att visas när enheter annonserar.';
@override
String get contacts_unread => 'Oläst';
@override
String get contacts_searchContactsNoNumber => 'Sök kontakter...';
@override
String contacts_searchContacts(int number, String str) {
return 'Sök kontakter...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Sök $number$str Favoriter...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Sök $number$str användare...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Sök $number$str upprepningsenheter...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Sök $number$str Room-servrar...';
}
String get contacts_searchContacts => 'Sök kontakter...';
@override
String get contacts_noUnreadContacts => 'Inga oinlästa kontakter';
@@ -965,12 +767,6 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get channels_editChannel => 'Redigera kanal';
@override
String get channels_muteChannel => 'Tysta kanal';
@override
String get channels_unmuteChannel => 'Slå på ljud för kanal';
@override
String get channels_deleteChannel => 'Ta bort kanal';
@@ -979,11 +775,6 @@ class AppLocalizationsSv extends AppLocalizations {
return 'Radera \"$name\"? Detta kan inte ångras.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Det gick inte att ta bort kanalen \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Kanalen \"$name\" raderad';
@@ -1272,9 +1063,6 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get chat_pathManagement => 'Stigarhantering';
@override
String get chat_ShowAllPaths => 'Visa alla vägar';
@override
String get chat_routingMode => 'Ruttläge';
@@ -1431,12 +1219,6 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get map_title => 'Nodkarta';
@override
String get map_lineOfSight => 'Synlinje';
@override
String get map_losScreenTitle => 'Synlinje';
@override
String get map_noNodesWithLocation => 'Inga noder med platsinformation';
@@ -1494,9 +1276,6 @@ 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';
@@ -1557,16 +1336,6 @@ 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';
@@ -1579,18 +1348,6 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get map_manageRepeater => 'Hantera Upprepare';
@override
String get map_tapToAdd => 'Tryck på noder för att lägga till dem i banan.';
@override
String get map_runTrace => 'Kör spårsökning';
@override
String get map_removeLast => 'Ta bort sista';
@override
String get map_pathTraceCancelled => 'Sökvägsspårning avbruten.';
@override
String get mapCache_title => 'Offline Kartcache';
@@ -1883,10 +1640,10 @@ class AppLocalizationsSv extends AppLocalizations {
String get repeater_cliSubtitle => 'Skicka kommandon till repetitorn';
@override
String get repeater_neighbors => 'Grannar';
String get repeater_neighbours => 'Grannar';
@override
String get repeater_neighborsSubtitle => 'Visa noll hoppgrannar.';
String get repeater_neighboursSubtitle => 'Visa noll hoppgrannar.';
@override
String get repeater_settings => 'Inställningar';
@@ -2577,7 +2334,7 @@ class AppLocalizationsSv extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbors => 'Upprepar grannar';
String get neighbors_repeatersNeighbours => 'Upprepar grannar';
@override
String get neighbors_noData => 'Inga grannuppgifter finns tillgängliga.';
@@ -2885,15 +2642,6 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get listFilter_all => 'Alla';
@override
String get listFilter_favorites => 'Favoriter';
@override
String get listFilter_addToFavorites => 'Lägg till i favoriter';
@override
String get listFilter_removeFromFavorites => 'Ta bort från favoriter';
@override
String get listFilter_users => 'Användare';
@@ -2925,145 +2673,6 @@ class AppLocalizationsSv extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'En eller flera av humlen saknar en plats!';
@override
String get pathTrace_clearTooltip => 'Rensa väg';
@override
String get losSelectStartEnd => 'Välj start- och slutnoder för LOS.';
@override
String losRunFailed(String error) {
return 'Synlinjekontroll misslyckades: $error';
}
@override
String get losClearAllPoints => 'Rensa alla punkter';
@override
String get losRunToViewElevationProfile => 'Kör LOS för att se höjdprofil';
@override
String get losMenuTitle => 'LOS-menyn';
@override
String get losMenuSubtitle =>
'Tryck på noder eller tryck länge på kartan för anpassade punkter';
@override
String get losShowDisplayNodes => 'Visa displaynoder';
@override
String get losCustomPoints => 'Anpassade poäng';
@override
String losCustomPointLabel(int index) {
return 'Anpassad $index';
}
@override
String get losPointA => 'Punkt A';
@override
String get losPointB => 'Punkt B';
@override
String losAntennaA(String value, String unit) {
return 'Antenn A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antenn B: $value $unit';
}
@override
String get losRun => 'Kör LOS';
@override
String get losNoElevationData => 'Inga höjddata';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, rensa LOS, min clearance $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, blockerad av $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: kollar...';
@override
String get losStatusNoData => 'LOS: inga data';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total rensa, $blocked blockerad, $unknown okänd';
}
@override
String get losErrorElevationUnavailable =>
'Höjddata är inte tillgänglig för ett eller flera prover.';
@override
String get losErrorInvalidInput =>
'Ogiltiga poäng/höjddata för LOS-beräkning.';
@override
String get losRenameCustomPoint => 'Byt namn på anpassad punkt';
@override
String get losPointName => 'Punktnamn';
@override
String get losShowPanelTooltip => 'Visa LOS-panelen';
@override
String get losHidePanelTooltip => 'Dölj LOS-panelen';
@override
String get losElevationAttribution => 'Höjddata: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Radiohorisont';
@override
String get losLegendLosBeam => 'Siktlinje';
@override
String get losLegendTerrain => 'Terräng';
@override
String get losFrequencyLabel => 'Frekvens';
@override
String get losFrequencyInfoTooltip => 'Visa detaljer om beräkningen';
@override
String get losFrequencyDialogTitle => 'Beräkning av radiohorisonten';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Med start från k=$baselineK vid $baselineFreq MHz, justerar beräkningen k-faktorn för det aktuella $frequencyMHz MHz-bandet, som definierar den böjda radiohorisonten.';
}
@override
String get contacts_pathTrace => 'Path Trace';
@@ -3232,88 +2841,4 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'meshcore-open export av GPX-kartdata';
@override
String get snrIndicator_nearByRepeaters => 'Närliggande uppreparstationer';
@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?';
}
+15 -498
View File
@@ -38,9 +38,6 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get common_delete => 'Видалити';
@override
String get common_deleteAll => 'Видалити все';
@override
String get common_close => 'Закрити';
@@ -111,135 +108,6 @@ 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 => 'Пошук пристроїв...';
@@ -275,23 +143,6 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get scanner_scan => 'Сканувати';
@override
String get scanner_bluetoothOff => 'Bluetooth вимкнено';
@override
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';
@override
String get device_quickSwitch => 'Швидке перемикання';
@@ -371,13 +222,6 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get settings_longitude => 'Довгота';
@override
String get settings_contactSettings => 'Налаштування контактів';
@override
String get settings_contactSettingsSubtitle =>
'Налаштування для додавання контактів';
@override
String get settings_privacyMode => 'Режим приватності';
@@ -468,10 +312,6 @@ class AppLocalizationsUk extends AppLocalizations {
String get settings_aboutDescription =>
'Клієнт Flutter з відкритим вихідним кодом для пристроїв мережі MeshCore LoRa.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Дані про висоту LOS: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Ім\'я';
@@ -496,6 +336,15 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get settings_presets => 'Попередні налаштування';
@override
String get settings_preset915Mhz => '915 МГц';
@override
String get settings_preset868Mhz => '868 МГц';
@override
String get settings_preset433Mhz => '433 МГц';
@override
String get settings_frequency => 'Частота (МГц)';
@@ -524,15 +373,10 @@ class AppLocalizationsUk extends AppLocalizations {
String get settings_txPowerInvalid => 'Некоректна потужність TX (0-22 дБм)';
@override
String get settings_clientRepeat => 'Автономна система';
String get settings_longRange => 'Дальній діапазон';
@override
String get settings_clientRepeatSubtitle =>
'Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.';
@override
String get settings_clientRepeatFreqWarning =>
'Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.';
String get settings_fastSpeed => 'Висока швидкість';
@override
String settings_error(String message) {
@@ -608,14 +452,6 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get appSettings_languageUk => 'Українська';
@override
String get appSettings_enableMessageTracing =>
'Увімкнути відстеження повідомлень';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Показувати детальні метадані про маршрутизацію та час для повідомлень';
@override
String get appSettings_notifications => 'Сповіщення';
@@ -776,15 +612,6 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Офлайн-кеш карти';
@override
String get appSettings_unitsTitle => 'одиниці';
@override
String get appSettings_unitsMetric => 'Метричний (м / км)';
@override
String get appSettings_unitsImperial => 'Імперська (ft / mi)';
@override
String get appSettings_noAreaSelected => 'Область не вибрано';
@@ -822,35 +649,7 @@ class AppLocalizationsUk extends AppLocalizations {
'Контакти з\'являться, коли пристрої надішлють оголошення.';
@override
String get contacts_unread => 'Непрочитане';
@override
String get contacts_searchContactsNoNumber => 'Пошук контактів...';
@override
String contacts_searchContacts(int number, String str) {
return 'Пошук контактів...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Пошук $number$str улюблених...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Пошук $number$str користувачів...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Пошук $number$str ретрансляторів...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Пошук $number$str серверів кімнат...';
}
String get contacts_searchContacts => 'Пошук контактів...';
@override
String get contacts_noUnreadContacts => 'Немає непрочитаних контактів';
@@ -975,12 +774,6 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get channels_editChannel => 'Редагувати канал';
@override
String get channels_muteChannel => 'Вимкнути сповіщення каналу';
@override
String get channels_unmuteChannel => 'Увімкнути сповіщення каналу';
@override
String get channels_deleteChannel => 'Видалити канал';
@@ -989,11 +782,6 @@ class AppLocalizationsUk extends AppLocalizations {
return 'Видалити $name? Це не можна скасувати.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Не вдалося видалити канал \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Канал «$name» видалено';
@@ -1281,9 +1069,6 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get chat_pathManagement => 'Керування шляхами';
@override
String get chat_ShowAllPaths => 'Показати всі шляхи';
@override
String get chat_routingMode => 'Режим маршрутизації';
@@ -1446,12 +1231,6 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get map_title => 'Карта вузлів';
@override
String get map_lineOfSight => 'Пряма видимість';
@override
String get map_losScreenTitle => 'Пряма видимість';
@override
String get map_noNodesWithLocation =>
'Немає вузлів з даними про розташування';
@@ -1510,9 +1289,6 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get map_shareMarkerHere => 'Поділитися маркером тут';
@override
String get map_setAsMyLocation => 'Встановити моє місцезнаходження';
@override
String get map_pinLabel => 'Мітка піна';
@@ -1573,16 +1349,6 @@ 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 => 'Час останньої активності';
@@ -1595,18 +1361,6 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get map_manageRepeater => 'Керувати ретранслятором';
@override
String get map_tapToAdd => 'Натисніть на вузли, щоб додати їх до шляху';
@override
String get map_runTrace => 'Виконати трасування шляху';
@override
String get map_removeLast => 'Видалити останній';
@override
String get map_pathTraceCancelled => 'Відмінується трасування шляху';
@override
String get mapCache_title => 'Офлайн-кеш карти';
@@ -1903,10 +1657,10 @@ class AppLocalizationsUk extends AppLocalizations {
String get repeater_cliSubtitle => 'Надіслати команди ретранслятору';
@override
String get repeater_neighbors => 'Сусіди';
String get repeater_neighbours => 'Сусіди';
@override
String get repeater_neighborsSubtitle =>
String get repeater_neighboursSubtitle =>
'Показати сусідів нульового стрибка.';
@override
@@ -2608,7 +2362,7 @@ class AppLocalizationsUk extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbors => 'Ретранслятори-сусіди';
String get neighbors_repeatersNeighbours => 'Ретранслятори-сусіди';
@override
String get neighbors_noData => 'Дані про сусідів недоступні.';
@@ -2924,15 +2678,6 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get listFilter_all => 'Все';
@override
String get listFilter_favorites => 'Улюблені';
@override
String get listFilter_addToFavorites => 'Додати до улюблених';
@override
String get listFilter_removeFromFavorites => 'Видалити зі списку улюблених';
@override
String get listFilter_users => 'Користувачі';
@@ -2964,148 +2709,6 @@ class AppLocalizationsUk extends AppLocalizations {
String get pathTrace_someHopsNoLocation =>
'Одне або більше хмелів відсутнє місце розташування!';
@override
String get pathTrace_clearTooltip => 'Очистити шлях';
@override
String get losSelectStartEnd =>
'Виберіть початковий і кінцевий вузли для LOS.';
@override
String losRunFailed(String error) {
return 'Помилка перевірки прямої видимості: $error';
}
@override
String get losClearAllPoints => 'Очистити всі пункти';
@override
String get losRunToViewElevationProfile =>
'Запустіть LOS, щоб переглянути профіль висоти';
@override
String get losMenuTitle => 'Меню LOS';
@override
String get losMenuSubtitle =>
'Торкніться вузлів або утримуйте карту, щоб отримати власні точки';
@override
String get losShowDisplayNodes => 'Показати вузли відображення';
@override
String get losCustomPoints => 'Користувальницькі точки';
@override
String losCustomPointLabel(int index) {
return 'Спеціальний $index';
}
@override
String get losPointA => 'Точка А';
@override
String get losPointB => 'Точка Б';
@override
String losAntennaA(String value, String unit) {
return 'Антена A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Антена B: $value $unit';
}
@override
String get losRun => 'Запустіть LOS';
@override
String get losNoElevationData => 'Немає даних про висоту';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, чистий LOS, мінімальний зазор $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, заблоковано $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: перевірка...';
@override
String get losStatusNoData => 'LOS: немає даних';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total очищено, $blocked заблоковано, $unknown невідомо';
}
@override
String get losErrorElevationUnavailable =>
'Дані про висоту недоступні для одного чи кількох зразків.';
@override
String get losErrorInvalidInput =>
'Недійсні дані про точки/висоту для розрахунку LOS.';
@override
String get losRenameCustomPoint => 'Перейменуйте спеціальну точку';
@override
String get losPointName => 'Назва точки';
@override
String get losShowPanelTooltip => 'Показати панель LOS';
@override
String get losHidePanelTooltip => 'Приховати панель LOS';
@override
String get losElevationAttribution =>
'Дані про висоту: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Радіогоризонт';
@override
String get losLegendLosBeam => 'Лінія прямої видимості';
@override
String get losLegendTerrain => 'Рельєф';
@override
String get losFrequencyLabel => 'Частота';
@override
String get losFrequencyInfoTooltip => 'Переглянути деталі розрахунку';
@override
String get losFrequencyDialogTitle => 'Розрахунок радіогоризонту';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Починаючи з k=$baselineK на $baselineFreq МГц, обчислення коригує k-фактор для поточного діапазону $frequencyMHz МГц, який визначає викривлену межу радіогоризонту.';
}
@override
String get contacts_pathTrace => 'Трасування шляхів';
@@ -3283,90 +2886,4 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get settings_gpxExportShareSubject =>
'експорт даних карти meshcore-open у форматі GPX';
@override
String get snrIndicator_nearByRepeaters => 'Ближні ретранслятори';
@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 =>
'Ви впевнені, що хочете видалити всі виявлені контакти?';
}
File diff suppressed because it is too large Load Diff
+11 -307
View File
@@ -1,12 +1,4 @@
{
"channels_channelDeleteFailed": "Kan kanaal {name} niet verwijderen",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "nl",
"appTitle": "MeshCore Open",
"nav_contacts": "Contacten",
@@ -139,6 +131,9 @@
"settings_infoContactsCount": "Aantal Contacten",
"settings_infoChannelCount": "Aantal Kanalen",
"settings_presets": "Presets",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
"settings_preset433Mhz": "433 MHz",
"settings_frequency": "Frequentie (MHz)",
"settings_frequencyHelper": "300,0 - 2500,0",
"settings_frequencyInvalid": "Ongeldige frequentie (300-2500 MHz)",
@@ -148,6 +143,8 @@
"settings_txPower": "TX Vermogen (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Ongeldige TX-vermogen (0-22 dBm)",
"settings_longRange": "Lange Afstand",
"settings_fastSpeed": "Hoge Snelheid",
"settings_error": "Fout: {message}",
"@settings_error": {
"placeholders": {
@@ -342,8 +339,6 @@
"channels_publicChannel": "Open kanaal",
"channels_privateChannel": "Private kanaal",
"channels_editChannel": "Kanaal bewerken",
"channels_muteChannel": "Kanaal dempen",
"channels_unmuteChannel": "Kanaal dempen opheffen",
"channels_deleteChannel": "Kanaal verwijderen",
"channels_deleteChannelConfirm": "Verwijderen \"{name}\"? Dit kan niet worden teruggedraaid.",
"@channels_deleteChannelConfirm": {
@@ -1361,12 +1356,12 @@
}
}
},
"repeater_neighbors": "Buren",
"repeater_neighborsSubtitle": "Bekijk nul hops buren.",
"repeater_neighbours": "Buren",
"repeater_neighboursSubtitle": "Bekijk nul hops buren.",
"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_repeatersNeighbours": "Herhalingen Buren",
"neighbors_noData": "Geen gegevens van buren beschikbaar.",
"channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.",
"channels_createPrivateChannel": "Maak een Privé Kanaal",
@@ -1565,8 +1560,6 @@
"contacts_floodAdvert": "Overstromingsadvertentie",
"contacts_copyAdvertToClipboard": "Advert naar klembord kopiëren",
"appSettings_languageRu": "Russisch",
"appSettings_enableMessageTracing": "Berichttracking inschakelen",
"appSettings_enableMessageTracingSubtitle": "Gedetailleerde routerings- en timing-metadata voor berichten weergeven",
"contacts_clipboardEmpty": "Knipbord is leeg.",
"contacts_addContactFromClipboard": "Contact uit klembord toevoegen",
"contacts_contactImported": "Contact is geïmporteerd.",
@@ -1582,6 +1575,7 @@
"notification_newNodesCount": "{count} {count, plural, =1{nieuw knooppunt} other{nieuwe knooppunten}}",
"notification_newTypeDiscovered": "Nieuw {contactType} ontdekt",
"notification_receivedNewMessage": "Nieuw bericht ontvangen",
"contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden",
"settings_gpxExportRepeatersSubtitle": "Exporteert repeaters / roomserver met een locatie naar GPX-bestand.",
"settings_gpxExportRepeaters": "Exporteer repeaters / roomserver naar GPX",
"settings_gpxExportSuccess": "Succesvol GPX-bestand geëxporteerd.",
@@ -1597,296 +1591,6 @@
"settings_gpxExportAllContacts": "Alle contactlocaties",
"settings_gpxExportShareText": "Kaartgegevens geëxporteerd uit meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open GPX kaartgegevens exporteren",
"pathTrace_someHopsNoLocation": "Een of meer van de hops ontbreken een locatie!",
"map_removeLast": "Verwijder Laatste",
"pathTrace_clearTooltip": "Weg wissen",
"map_pathTraceCancelled": "Pad traceren geannuleerd",
"map_tapToAdd": "Tik op knooppunten om ze toe te voegen aan het pad",
"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_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",
"appSettings_unitsMetric": "Metrisch (m / km)",
"appSettings_unitsImperial": "Imperiaal (ft / mi)",
"map_lineOfSight": "Zichtlijn",
"map_losScreenTitle": "Zichtlijn",
"losSelectStartEnd": "Selecteer begin- en eindknooppunten voor LOS.",
"losRunFailed": "Zichtlijncontrole mislukt: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Wis alle punten",
"losRunToViewElevationProfile": "Voer LOS uit om het hoogteprofiel te bekijken",
"losMenuTitle": "LOS-menu",
"losMenuSubtitle": "Tik op knooppunten of druk lang op de kaart voor aangepaste punten",
"losShowDisplayNodes": "Toon weergaveknooppunten",
"losCustomPoints": "Aangepaste punten",
"losCustomPointLabel": "Aangepast {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Punt A",
"losPointB": "Punt B",
"losAntennaA": "Antenne A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antenne B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Voer LOS uit",
"losNoElevationData": "Geen hoogtegegevens",
"losProfileClear": "{distance} {distanceUnit}, vrije LOS, min. vrije ruimte {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, geblokkeerd door {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: controleren...",
"losStatusNoData": "LOS: geen gegevens",
"losStatusSummary": "LOS: {clear}/{total} gewist, {blocked} geblokkeerd, {unknown} onbekend",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Hoogtegegevens niet beschikbaar voor een of meer monsters.",
"losErrorInvalidInput": "Ongeldige punten/hoogtegegevens voor LOS-berekening.",
"losRenameCustomPoint": "Hernoem aangepast punt",
"losPointName": "Puntnaam",
"losShowPanelTooltip": "Toon LOS-paneel",
"losHidePanelTooltip": "LOS-paneel verbergen",
"losElevationAttribution": "Hoogtegegevens: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Radiohorizon",
"losLegendLosBeam": "Zichtlijn",
"losLegendTerrain": "Terrein",
"losFrequencyLabel": "Frequentie",
"losFrequencyInfoTooltip": "Bekijk details van de berekening",
"losFrequencyDialogTitle": "Berekening van de radiohorizon",
"losFrequencyDialogDescription": "Beginnend met k={baselineK} bij {baselineFreq} MHz, wordt bij de berekening de k-factor aangepast voor de huidige {frequencyMHz} MHz-band, die de gebogen radiohorizonkap definieert.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_removeFromFavorites": "Verwijderen uit favorieten",
"listFilter_favorites": "Favorieten",
"listFilter_addToFavorites": "Toevoegen aan favorieten",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_unread": "Ongelezen",
"contacts_searchRepeaters": "Zoek {number}{str} Repeaters...",
"contacts_searchContactsNoNumber": "Zoek contacten...",
"contacts_searchUsers": "Zoek {number}{str} gebruikers...",
"contacts_searchFavorites": "Zoek {number}{str} favorieten...",
"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"
"pathTrace_someHopsNoLocation": "Een of meer van de hops ontbreken een locatie!"
}
+11 -307
View File
@@ -1,12 +1,4 @@
{
"channels_channelDeleteFailed": "Nie udało się usunąć kanału \"{name}\"",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "pl",
"appTitle": "MeshCore Open",
"nav_contacts": "Kontakty",
@@ -139,6 +131,9 @@
"settings_infoContactsCount": "Liczba kontaktów",
"settings_infoChannelCount": "Liczba kanałów",
"settings_presets": "Preset",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
"settings_preset433Mhz": "433 MHz",
"settings_frequency": "Częstotliwość (MHz)",
"settings_frequencyHelper": "300,0 - 2500,0",
"settings_frequencyInvalid": "Nieprawidłowa częstotliwość (300-2500 MHz)",
@@ -148,6 +143,8 @@
"settings_txPower": "TX Moc (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Nieprawidłowa moc TX (0-22 dBm)",
"settings_longRange": "Długi zasięg",
"settings_fastSpeed": "Szybka prędkość",
"settings_error": "Błąd: {message}",
"@settings_error": {
"placeholders": {
@@ -342,8 +339,6 @@
"channels_publicChannel": "Kanał publiczny",
"channels_privateChannel": "Prywatny kanał",
"channels_editChannel": "Edytuj kanał",
"channels_muteChannel": "Wycisz kanał",
"channels_unmuteChannel": "Wyłącz wyciszenie kanału",
"channels_deleteChannel": "Usuń kanał",
"channels_deleteChannelConfirm": "Usuń \"{name}\"? Nie można tego cofnąć.",
"@channels_deleteChannelConfirm": {
@@ -1361,12 +1356,12 @@
}
}
},
"repeater_neighbors": "Sąsiedzi",
"repeater_neighborsSubtitle": "Wyświetl sąsiedztwo zerowych hopów.",
"repeater_neighbours": "Sąsiedzi",
"repeater_neighboursSubtitle": "Wyświetl sąsiedztwo zerowych hopów.",
"neighbors_receivedData": "Otrzymano dane sąsiedztwa",
"neighbors_requestTimedOut": "Sąsiedzi proszą o wyłączenie timingu.",
"neighbors_errorLoading": "Błąd podczas ładowania sąsiadów: {error}",
"neighbors_repeatersNeighbors": "Powtarzacze Sąsiedzi",
"neighbors_repeatersNeighbours": "Powtarzacze Sąsiedzi",
"neighbors_noData": "Brak danych dotyczących sąsiadów.",
"channels_joinPrivateChannelDesc": "Ręcznie wprowadź klucz tajny.",
"channels_createPrivateChannel": "Utwórz Prywatny Kanał",
@@ -1560,8 +1555,6 @@
"contacts_chatTraceRoute": "Śledź trasę promienia",
"appSettings_languageRu": "Rosyjski",
"appSettings_languageUk": "Ukraińska",
"appSettings_enableMessageTracing": "Włącz śledzenie wiadomości",
"appSettings_enableMessageTracingSubtitle": "Pokaż szczegółowe metadane trasowania i czasu dla wiadomości",
"contacts_contactImportFailed": "Kontakt nie został zaimportowany.",
"contacts_zeroHopAdvert": "Reklama Zero Hop",
"contacts_floodAdvert": "Reklama powodziowa",
@@ -1582,6 +1575,7 @@
"notification_newNodesCount": "{count} {count, plural, =1{nowy węzeł} few{nowe węzły} many{nowych węzłów} other{nowych węzłów}}",
"notification_newTypeDiscovered": "Nowy {contactType} wykryty",
"notification_receivedNewMessage": "Otrzymano nową wiadomość",
"contacts_zeroHopContactAdvertFailed": "Nie udało się wysłać kontaktu.",
"settings_gpxExportContacts": "Eksportuj towarzyszy do GPX",
"settings_gpxExportRepeaters": "Eksportuj powtórki / serwer pokojowy do GPX",
"settings_gpxExportRepeatersSubtitle": "Eksportuje powtarzacze / roomserver z lokalizacją do pliku GPX.",
@@ -1597,296 +1591,6 @@
"settings_gpxExportChat": "Lokalizacje towarzyszy",
"settings_gpxExportShareText": "Dane mapy wyeksportowane z meshcore-open",
"settings_gpxExportShareSubject": "Eksport danych mapy GPX meshcore-open",
"pathTrace_someHopsNoLocation": "Jeden lub więcej z chmieli nie ma określonej lokalizacji!",
"map_pathTraceCancelled": "Śledzenie ścieżki anulowano.",
"map_runTrace": "Uruchom ślad ścieżki",
"pathTrace_clearTooltip": "Wyczyść ścieżkę",
"map_removeLast": "Usuń ostatni",
"map_tapToAdd": "Kliknij na węzły, aby dodać je do ścieżki.",
"scanner_bluetoothOffMessage": "Prosimy włączyć Bluetooth, aby przeskanować urządzenia.",
"scanner_chromeRequired": "Wymagana przeglądarka Chrome",
"scanner_chromeRequiredMessage": "Ta aplikacja internetowa wymaga przeglądarki Google Chrome lub opartej na Chromium do obsługi Bluetooth.",
"scanner_bluetoothOff": "Bluetooth jest wyłączony",
"scanner_enableBluetooth": "Włącz Bluetooth",
"snrIndicator_lastSeen": "Ostatnio widziany",
"snrIndicator_nearByRepeaters": "Nadajniki w pobliżu",
"chat_ShowAllPaths": "Pokaż wszystkie ścieżki",
"settings_clientRepeatSubtitle": "Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.",
"settings_clientRepeat": "Powtórzenie: Niezależne od sieci",
"settings_clientRepeatFreqWarning": "Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz.",
"settings_aboutOpenMeteoAttribution": "Dane wysokościowe LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Jednostki",
"appSettings_unitsMetric": "Metryczne (m / km)",
"appSettings_unitsImperial": "Imperialne (ft / mi)",
"map_lineOfSight": "Linia wzroku",
"map_losScreenTitle": "Linia wzroku",
"losSelectStartEnd": "Wybierz węzły początkowe i końcowe dla LOS.",
"losRunFailed": "Sprawdzenie pola widzenia nie powiodło się: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Wyczyść wszystkie punkty",
"losRunToViewElevationProfile": "Uruchom LOS, aby wyświetlić profil wysokości",
"losMenuTitle": "Menu LOS",
"losMenuSubtitle": "Stuknij węzły lub naciśnij i przytrzymaj mapę, aby uzyskać niestandardowe punkty",
"losShowDisplayNodes": "Pokaż węzły wyświetlające",
"losCustomPoints": "Punkty niestandardowe",
"losCustomPointLabel": "Niestandardowe {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Punkt A",
"losPointB": "Punkt B",
"losAntennaA": "Antena A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antena B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Uruchom LOS-a",
"losNoElevationData": "Brak danych o wysokości",
"losProfileClear": "{distance} {distanceUnit}, czysty LOS, minimalny prześwit {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, zablokowane przez {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: sprawdzam...",
"losStatusNoData": "LOS: brak danych",
"losStatusSummary": "LOS: {clear}/{total} jasne, {blocked} zablokowane, {unknown} nieznane",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Dane dotyczące wysokości są niedostępne dla jednej lub większej liczby próbek.",
"losErrorInvalidInput": "Nieprawidłowe dane punktów/wysokości do obliczenia LOS.",
"losRenameCustomPoint": "Zmień nazwę punktu niestandardowego",
"losPointName": "Nazwa punktu",
"losShowPanelTooltip": "Pokaż panel LOS",
"losHidePanelTooltip": "Ukryj panel LOS",
"losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Horyzont radiowy",
"losLegendLosBeam": "Linia widoczności",
"losLegendTerrain": "Teren",
"losFrequencyLabel": "Częstotliwość",
"losFrequencyInfoTooltip": "Zobacz szczegóły obliczenia",
"losFrequencyDialogTitle": "Obliczanie horyzontu radiowego",
"losFrequencyDialogDescription": "Zaczynając od k={baselineK} przy {baselineFreq} MHz, obliczenia korygują współczynnik k dla bieżącego pasma {frequencyMHz} MHz, które definiuje zakrzywiony limit horyzontu radiowego.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_removeFromFavorites": "Usuń z ulubionych",
"listFilter_addToFavorites": "Dodaj do ulubionych",
"listFilter_favorites": "Ulubione",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_unread": "Nieprzeczytane",
"contacts_searchContactsNoNumber": "Wyszukaj kontakty...",
"contacts_searchFavorites": "Wyszukaj {number}{str} ulubione...",
"contacts_searchRoomServers": "Wyszukaj {number}{str} serwerów Room...",
"contacts_searchUsers": "Wyszukaj {number}{str} Użytkowników...",
"contacts_searchRepeaters": "Wyszukaj {number}{str} powtórników...",
"contactsSettings_title": "Ustawienia kontaktów",
"settings_contactSettingsSubtitle": "Ustawienia dotyczące sposobu dodawania kontaktów",
"contactsSettings_autoAddUsersSubtitle": "Pozwól towarzyszowi automatycznie dodawać znalezione użytkowników.",
"contactsSettings_autoAddRepeatersTitle": "Automatyczne dodawanie powtarzalników",
"contactsSettings_autoAddRepeatersSubtitle": "Zezwól na automatyczne dodawanie odkrytych repeaterów przez towarzysza.",
"contactsSettings_autoAddRoomServersTitle": "Automatycznie dodaj serwery pokojowe",
"contactsSettings_autoAddUsersTitle": "Automatycznie dodaj użytkowników",
"settings_contactSettings": "Ustawienia kontaktowe",
"contactsSettings_otherTitle": "Inne ustawienia związane z kontaktami",
"contactsSettings_autoAddTitle": "Automatyczne odnajdywanie",
"contactsSettings_autoAddRoomServersSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie znalezionych serwerów pokojowych.",
"contactsSettings_autoAddSensorsTitle": "Automatycznie dodaj czujniki",
"discoveredContacts_searchHint": "Wyszukaj odkryte kontakty",
"discoveredContacts_contactAdded": "Kontakt dodany",
"discoveredContacts_addContact": "Dodaj kontakt",
"discoveredContacts_copyContact": "Kopiuj kontakt do schowka",
"contactsSettings_overwriteOldestTitle": "Nadpisz najstarszy",
"discoveredContacts_Title": "Odkryte Kontakty",
"contactsSettings_autoAddSensorsSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie wykrytych czujników.",
"discoveredContacts_noMatching": "Brak pasujących kontaktów",
"discoveredContacts_deleteContact": "Usuń kontakt",
"contactsSettings_overwriteOldestSubtitle": "Gdy lista kontaktów jest pełna, najstarszy nieulubiony kontakt zostanie zastąpiony.",
"common_deleteAll": "Usuń wszystko",
"discoveredContacts_deleteContactAllContent": "Czy na pewno chcesz usunąć wszystkie znalezione kontakty?",
"discoveredContacts_deleteContactAll": "Usuń wszystkie odkryte kontakty",
"map_guessedLocation": "Wydana lokalizacja",
"map_showGuessedLocations": "Wyświetl lokalizacje zgadanych węzłów",
"usbScreenSubtitle": "Wybierz wykryty urządzenie szeregowe i podłącz je bezpośrednio do swojego węzła MeshCore.",
"usbScreenTitle": "Połącz przez USB",
"connectionChoiceUsbLabel": "USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenStatus": "Wybierz urządzenie USB",
"usbScreenNote": "Port szeregowy USB jest aktywny na urządzeniach z systemem Android i platformach stacjonarnych, które go obsługują.",
"usbScreenEmptyState": "Nie znaleziono żadnych urządzeń USB. Podłącz jedno i zaktualizuj.",
"usbErrorPermissionDenied": "Zostało odrzucone żądanie dostępu przez USB.",
"usbErrorDeviceMissing": "Wybór urządzenia USB już nie jest dostępny.",
"usbErrorInvalidPort": "Wybierz prawidłowe urządzenie USB.",
"usbErrorBusy": "Kolejne żądanie połączenia przez USB jest już w trakcie realizacji.",
"usbErrorNotConnected": "Brak podłączonego urządzenia USB.",
"usbErrorOpenFailed": "Nie udało się otworzyć wybranego urządzenia USB.",
"usbErrorConnectFailed": "Nie udało się połączyć z wybranym urządzeniem USB.",
"usbErrorUnsupported": "Port szeregowy USB nie jest obsługiwany na tym urządzeniu.",
"usbErrorAlreadyActive": "Połączenie USB jest już aktywne.",
"usbErrorNoDeviceSelected": "Nie został wybrany żaden urządzenie USB.",
"usbErrorPortClosed": "Połączenie USB nie jest aktywne.",
"usbFallbackDeviceName": "Urządzenie do komunikacji przez sieć (seria)",
"@usbConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"usbStatus_searching": "Wyszukiwanie urządzeń USB...",
"usbStatus_connecting": "Połączenie z urządzeniem USB...",
"usbStatus_notConnected": "Wybierz urządzenie USB",
"usbConnectionFailed": "Błąd połączenia USB: {error}",
"usbErrorConnectTimedOut": "Połączenie nie zostało nawiązane. Upewnij się, że urządzenie posiada oprogramowanie \"USB Companion\".",
"@tcpStatus_connectingTo": {
"placeholders": {
"endpoint": {
"type": "String"
}
}
},
"@tcpConnectionFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"connectionChoiceTcpLabel": "TCP",
"tcpHostHint": "192.168.40.10",
"tcpScreenTitle": "Połącz się za pomocą protokołu TCP",
"tcpHostLabel": "Adres IP",
"tcpPortLabel": "Port",
"tcpPortHint": "5000",
"tcpStatus_notConnected": "Wprowadź adres URL i połącz",
"tcpStatus_connectingTo": "Połączenie z {endpoint}...",
"tcpErrorHostRequired": "Wymagana jest adresa IP.",
"tcpErrorPortInvalid": "Numer portu musi mieścić się w zakresie od 1 do 65535.",
"tcpErrorUnsupported": "Transport protokoł TCP nie jest obsługiwany na tym urządzeniu.",
"tcpErrorTimedOut": "Połączenie TCP zakończyło się bez powodzenia.",
"tcpConnectionFailed": "Błąd połączenia TCP: {error}",
"map_showDiscoveryContacts": "Pokaż kontakty odkrywania",
"map_setAsMyLocation": "Ustaw jako moje lokalizację"
"pathTrace_someHopsNoLocation": "Jeden lub więcej z chmieli nie ma określonej lokalizacji!"
}
+11 -307
View File
@@ -1,12 +1,4 @@
{
"channels_channelDeleteFailed": "Falha ao excluir o canal \"{name}\"",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "pt",
"appTitle": "MeshCore Open",
"nav_contacts": "Contactos",
@@ -139,6 +131,9 @@
"settings_infoContactsCount": "Número de Contatos",
"settings_infoChannelCount": "Número do Canal",
"settings_presets": "Presets",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
"settings_preset433Mhz": "433 MHz",
"settings_frequency": "Frequência (MHz)",
"settings_frequencyHelper": "300,0 - 2500,0",
"settings_frequencyInvalid": "Frequência inválida (300-2500 MHz)",
@@ -148,6 +143,8 @@
"settings_txPower": "TX Potência (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Potência de TX inválida (0-22 dBm)",
"settings_longRange": "Alcance Longo",
"settings_fastSpeed": "Velocidade Rápida",
"settings_error": "Erro: {message}",
"@settings_error": {
"placeholders": {
@@ -342,8 +339,6 @@
"channels_publicChannel": "Canal público",
"channels_privateChannel": "Canal privado",
"channels_editChannel": "Editar canal",
"channels_muteChannel": "Silenciar canal",
"channels_unmuteChannel": "Ativar canal",
"channels_deleteChannel": "Excluir canal",
"channels_deleteChannelConfirm": "Excluir \"{name}\"? Não pode ser desfeito.",
"@channels_deleteChannelConfirm": {
@@ -1361,12 +1356,12 @@
}
}
},
"repeater_neighbors": "Vizinhos",
"repeater_neighbours": "Vizinhos",
"neighbors_receivedData": "Dados dos Vizinhos Recebidos",
"repeater_neighborsSubtitle": "Visualizar vizinhos de salto zero.",
"repeater_neighboursSubtitle": "Visualizar vizinhos de salto zero.",
"neighbors_requestTimedOut": "Vizinhos solicitam tempo limite esgotado.",
"neighbors_errorLoading": "Erro ao carregar vizinhos: {error}",
"neighbors_repeatersNeighbors": "Repetidores Vizinhos",
"neighbors_repeatersNeighbours": "Repetidores Vizinhos",
"neighbors_noData": "Não estão disponíveis dados de vizinhos.",
"channels_createPrivateChannelDesc": "Protegido com uma chave secreta.",
"channels_joinPrivateChannelDesc": "Inserir uma chave secreta manualmente.",
@@ -1566,8 +1561,6 @@
"contacts_copyAdvertToClipboard": "Copiar Anúncio para Área de Transferência",
"contacts_addContactFromClipboard": "Adicionar Contato da Área de Transferência",
"appSettings_languageRu": "Russo",
"appSettings_enableMessageTracing": "Ativar rastreamento de mensagens",
"appSettings_enableMessageTracingSubtitle": "Mostrar metadados detalhados de roteamento e tempo para as mensagens",
"contacts_ShareContact": "Copiar contato para Área de Transferência",
"contacts_contactImportFailed": "Contato falhou ao ser importado.",
"contacts_zeroHopContactAdvertSent": "Enviou contato por anúncio.",
@@ -1582,6 +1575,7 @@
"notification_newNodesCount": "{count} {count, plural, =1{novo nó} other{novos nós}}",
"notification_newTypeDiscovered": "Novo {contactType} descoberto",
"notification_receivedNewMessage": "Nova mensagem recebida",
"contacts_zeroHopContactAdvertFailed": "Falha ao enviar contato.",
"settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala para GPX",
"settings_gpxExportRepeatersSubtitle": "Exporta repetidores / roomserver com localização para arquivo GPX.",
"settings_gpxExportSuccess": "Arquivo GPX exportado com sucesso.",
@@ -1597,296 +1591,6 @@
"settings_gpxExportAllContacts": "Todos os locais de contatos",
"settings_gpxExportShareText": "Dados do mapa exportados do meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open exportação de dados de mapa GPX",
"pathTrace_someHopsNoLocation": "Um ou mais dos lúpulos estão sem localização!",
"map_runTrace": "Executar Traçado de Caminho",
"map_pathTraceCancelled": "Rastreamento de caminho cancelado.",
"pathTrace_clearTooltip": "Limpar caminho",
"map_removeLast": "Remover Último",
"map_tapToAdd": "Toque nos nós para adicioná-los ao caminho.",
"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",
"settings_clientRepeatFreqWarning": "A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.",
"settings_clientRepeat": "Repetição sem rede",
"settings_clientRepeatSubtitle": "Permita que este dispositivo repita pacotes de rede para outros dispositivos.",
"settings_aboutOpenMeteoAttribution": "Dados de elevação LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Unidades",
"appSettings_unitsMetric": "Métrico (m/km)",
"appSettings_unitsImperial": "Imperial (ft/mi)",
"map_lineOfSight": "Linha de visão",
"map_losScreenTitle": "Linha de visão",
"losSelectStartEnd": "Selecione nós iniciais e finais para LOS.",
"losRunFailed": "Falha na verificação da linha de visão: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Limpe todos os pontos",
"losRunToViewElevationProfile": "Execute o LOS para visualizar o perfil de elevação",
"losMenuTitle": "Menu LOS",
"losMenuSubtitle": "Toque nos nós ou mantenha pressionado o mapa para obter pontos personalizados",
"losShowDisplayNodes": "Mostrar nós de exibição",
"losCustomPoints": "Pontos personalizados",
"losCustomPointLabel": "{index} personalizado",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Ponto A",
"losPointB": "Ponto B",
"losAntennaA": "Antena A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antena B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Executar LOS",
"losNoElevationData": "Sem dados de elevação",
"losProfileClear": "{distance} {distanceUnit}, limpar LOS, liberação mínima {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, bloqueado por {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: verificando...",
"losStatusNoData": "LOS: sem dados",
"losStatusSummary": "LOS: {clear}/{total} limpo, {blocked} bloqueado, {unknown} desconhecido",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Dados de elevação indisponíveis para uma ou mais amostras.",
"losErrorInvalidInput": "Dados de pontos/elevação inválidos para cálculo de LOS.",
"losRenameCustomPoint": "Renomear ponto personalizado",
"losPointName": "Nome do ponto",
"losShowPanelTooltip": "Mostrar painel LOS",
"losHidePanelTooltip": "Ocultar painel LOS",
"losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Horizonte de rádio",
"losLegendLosBeam": "Linha de visada",
"losLegendTerrain": "Terreno",
"losFrequencyLabel": "Frequência",
"losFrequencyInfoTooltip": "Ver detalhes do cálculo",
"losFrequencyDialogTitle": "Cálculo do horizonte de rádio",
"losFrequencyDialogDescription": "Começando em k={baselineK} em {baselineFreq} MHz, o cálculo ajusta o fator k para a banda atual de {frequencyMHz} MHz, que define o limite do horizonte de rádio curvo.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_addToFavorites": "Adicionar aos favoritos",
"listFilter_removeFromFavorites": "Remover da lista de favoritos",
"listFilter_favorites": "Favoritos",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_searchRepeaters": "Pesquisar {number}{str} Repetidores...",
"contacts_searchFavorites": "Pesquisar {number}{str} Favoritos...",
"contacts_searchUsers": "Pesquisar {number}{str} Usuários...",
"contacts_searchContactsNoNumber": "Pesquisar Contatos...",
"contacts_unread": "Não lido",
"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"
"pathTrace_someHopsNoLocation": "Um ou mais dos lúpulos estão sem localização!"
}
+11 -307
View File
@@ -1,12 +1,4 @@
{
"channels_channelDeleteFailed": "Не удалось удалить канал {name}.",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "ru",
"appTitle": "MeshCore Open",
"nav_contacts": "Контакты",
@@ -109,6 +101,9 @@
"settings_infoContactsCount": "Количество контактов",
"settings_infoChannelCount": "Количество каналов",
"settings_presets": "Пресеты",
"settings_preset915Mhz": "915 МГц",
"settings_preset868Mhz": "868 МГц",
"settings_preset433Mhz": "433 МГц",
"settings_frequency": "Частота (МГц)",
"settings_frequencyHelper": "300.0 2500.0",
"settings_frequencyInvalid": "Недопустимая частота (300–2500 МГц)",
@@ -118,6 +113,8 @@
"settings_txPower": "Мощность передачи (дБм)",
"settings_txPowerHelper": "0 22",
"settings_txPowerInvalid": "Недопустимая мощность передачи (0–22 дБм)",
"settings_longRange": "Дальний радиус",
"settings_fastSpeed": "Высокая скорость",
"settings_error": "Ошибка: {message}",
"appSettings_title": "Настройки приложения",
"appSettings_appearance": "Внешний вид",
@@ -234,8 +231,6 @@
"channels_publicChannel": "Публичный канал",
"channels_privateChannel": "Приватный канал",
"channels_editChannel": "Изменить канал",
"channels_muteChannel": "Отключить уведомления канала",
"channels_unmuteChannel": "Включить уведомления канала",
"channels_deleteChannel": "Удалить канал",
"channels_deleteChannelConfirm": "Удалить \"{name}\"? Это действие нельзя отменить.",
"channels_channelDeleted": "Канал \"{name}\" удалён",
@@ -477,8 +472,8 @@
"repeater_telemetrySubtitle": "Просмотр телеметрии датчиков и системной статистики",
"repeater_cli": "CLI",
"repeater_cliSubtitle": "Отправка команд репитеру",
"repeater_neighbors": "Соседи",
"repeater_neighborsSubtitle": "Просмотр соседей на нулевом хопе.",
"repeater_neighbours": "Соседи",
"repeater_neighboursSubtitle": "Просмотр соседей на нулевом хопе.",
"repeater_settings": "Настройки",
"repeater_settingsSubtitle": "Настройка параметров репитера",
"repeater_statusTitle": "Статус репитера",
@@ -671,7 +666,7 @@
"neighbors_receivedData": "Полученные данные о соседях",
"neighbors_requestTimedOut": "Время ожидания данных о соседях истекло.",
"neighbors_errorLoading": "Ошибка загрузки соседей: {error}",
"neighbors_repeatersNeighbors": "Соседи репитеров",
"neighbors_repeatersNeighbours": "Соседи репитеров",
"neighbors_noData": "Данные о соседях недоступны.",
"neighbors_unknownContact": "Неизвестный {pubkey}",
"neighbors_heardA ago": "Слышали: {time} назад",
@@ -804,8 +799,6 @@
"contacts_invalidAdvertFormat": "Недействительные контактные данные",
"contacts_zeroHopAdvert": "Реклама Zero Hop",
"appSettings_languageUk": "Українська",
"appSettings_enableMessageTracing": "Включить трассировку сообщений",
"appSettings_enableMessageTracingSubtitle": "Показывать подробные метаданные о маршрутизации и времени для сообщений",
"contacts_floodAdvert": "Рекламный поток",
"contacts_clipboardEmpty": "Буфер обмена пуст.",
"contacts_copyAdvertToClipboard": "Копировать рекламу в буфер обмена",
@@ -822,6 +815,7 @@
"notification_newNodesCount": "{count} {count, plural, =1{новый узел} few{новых узла} many{новых узлов} other{новых узлов}}",
"notification_newTypeDiscovered": "Обнаружен новый {contactType}",
"notification_receivedNewMessage": "Получено новое сообщение",
"contacts_zeroHopContactAdvertSent": "Отправлено сообщение по объявлению.",
"settings_gpxExportRepeaters": "Экспортировать рипитеры / сервер комнаты в GPX",
"settings_gpxExportRepeatersSubtitle": "Экспортирует ретрансляторы / сервер комнат с местоположением в файл GPX.",
"settings_gpxExportContacts": "Экспортировать спутников в GPX",
@@ -837,296 +831,6 @@
"settings_gpxExportNoContacts": "Нет контактов для экспорта.",
"settings_gpxExportShareText": "Данные карты экспортированы из meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open экспорт данных карты GPX",
"pathTrace_someHopsNoLocation": "Одному или нескольким хмелям не указано местоположение!",
"map_tapToAdd": "Нажимайте на узлы, чтобы добавить их в путь.",
"map_removeLast": "Удалить последний",
"map_pathTraceCancelled": "Отмена трассировки пути",
"pathTrace_clearTooltip": "Очистить путь",
"map_runTrace": "Запустить трассировку пути",
"scanner_enableBluetooth": "Включите Bluetooth",
"scanner_bluetoothOff": "Bluetooth выключен",
"scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства.",
"scanner_chromeRequired": "Требуется браузер Chrome",
"scanner_chromeRequiredMessage": "Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.",
"snrIndicator_nearByRepeaters": "Ближайшие ретрансляторы",
"snrIndicator_lastSeen": "Последний раз видели",
"chat_ShowAllPaths": "Показать все пути",
"settings_clientRepeatFreqWarning": "Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.",
"settings_clientRepeatSubtitle": "Позвольте этому устройству повторять пакеты данных для других устройств.",
"settings_clientRepeat": "Повторение \"вне сети\"",
"settings_aboutOpenMeteoAttribution": "Данные о высоте LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Единицы",
"appSettings_unitsMetric": "Метрическая (м/км)",
"appSettings_unitsImperial": "Имперская (ft / mi)",
"map_lineOfSight": "Линия видимости",
"map_losScreenTitle": "Линия видимости",
"losSelectStartEnd": "Выберите начальный и конечный узлы для LOS.",
"losRunFailed": "Проверка прямой видимости не удалась: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Очистить все точки",
"losRunToViewElevationProfile": "Запустите LOS, чтобы просмотреть профиль высот.",
"losMenuTitle": "ЛОС Меню",
"losMenuSubtitle": "Коснитесь узлов или нажмите и удерживайте карту для выбора пользовательских точек.",
"losShowDisplayNodes": "Показать узлы отображения",
"losCustomPoints": "Пользовательские точки",
"losCustomPointLabel": "Пользовательский {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Точка А",
"losPointB": "Точка Б",
"losAntennaA": "Антенна А: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Антенна Б: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Запустить ЛОС",
"losNoElevationData": "Нет данных о высоте",
"losProfileClear": "{distance} {distanceUnit}, свободная зона видимости, минимальный зазор {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, заблокирован {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "ЛОС: проверяю...",
"losStatusNoData": "ЛОС: нет данных",
"losStatusSummary": "LOS: {clear}/{total} очищено, {blocked} заблокировано, {unknown} неизвестно.",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Данные о высоте недоступны для одного или нескольких образцов.",
"losErrorInvalidInput": "Неверные данные о точках/высоте для расчета LOS.",
"losRenameCustomPoint": "Переименовать пользовательскую точку",
"losPointName": "Имя точки",
"losShowPanelTooltip": "Показать панель LOS",
"losHidePanelTooltip": "Скрыть панель LOS",
"losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Радиогоризонт",
"losLegendLosBeam": "Линия прямой видимости",
"losLegendTerrain": "Рельеф",
"losFrequencyLabel": "Частота",
"losFrequencyInfoTooltip": "Просмотреть детали расчёта",
"losFrequencyDialogTitle": "Расчёт радиогоризонта",
"losFrequencyDialogDescription": "Начиная с k={baselineK} на частоте {baselineFreq} МГц, расчет корректирует коэффициент k для текущего диапазона {frequencyMHz} МГц, который определяет изогнутую границу радиогоризонта.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_addToFavorites": "Добавить в избранное",
"listFilter_favorites": "Избранное",
"listFilter_removeFromFavorites": "Удалить из избранного",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_searchRepeaters": "Поиск {number}{str} ретрансляторов...",
"contacts_searchContactsNoNumber": "Поиск контактов...",
"contacts_unread": "Непрочитанное",
"contacts_searchRoomServers": "Поиск {number}{str} серверов комнат...",
"contacts_searchFavorites": "Поиск {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": "Установить мое местоположение"
"pathTrace_someHopsNoLocation": "Одному или нескольким хмелям не указано местоположение!"
}
+11 -307
View File
@@ -1,12 +1,4 @@
{
"channels_channelDeleteFailed": "Kanál \"{name}\" sa nepodarilo odstrániť",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "sk",
"appTitle": "MeshCore Open",
"nav_contacts": "Kontakty",
@@ -139,6 +131,9 @@
"settings_infoContactsCount": "Počet kontaktov",
"settings_infoChannelCount": "Počet kanálov",
"settings_presets": "Prednastavenia",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
"settings_preset433Mhz": "433 MHz",
"settings_frequency": "Frekvencia (MHz)",
"settings_frequencyHelper": "300,0 2500,0",
"settings_frequencyInvalid": "Neplatná frekvencia (300-2500 MHz)",
@@ -148,6 +143,8 @@
"settings_txPower": "TX Výkon (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Neplatná hodnota výkonu TX (0-22 dBm)",
"settings_longRange": "Dlhý dosah",
"settings_fastSpeed": "Rýchla rýchlosť",
"settings_error": "Chyba: {message}",
"@settings_error": {
"placeholders": {
@@ -342,8 +339,6 @@
"channels_publicChannel": "Veľké verejne kanály",
"channels_privateChannel": "Osobné kanál",
"channels_editChannel": "Upraviť kanál",
"channels_muteChannel": "Stlmiť kanál",
"channels_unmuteChannel": "Zrušiť stlmenie kanála",
"channels_deleteChannel": "Odstrániť kanál",
"channels_deleteChannelConfirm": "Odstrániť \"{name}\"? To sa nedá zrušiť.",
"@channels_deleteChannelConfirm": {
@@ -1361,12 +1356,12 @@
}
}
},
"repeater_neighborsSubtitle": "Zobraziť susedné body bez skokov.",
"repeater_neighboursSubtitle": "Zobraziť susedné body bez skokov.",
"neighbors_requestTimedOut": "Súďia žiadajú o časové ukončenie.",
"neighbors_receivedData": "Obdielo dáta suseda",
"repeater_neighbors": "Súsezný",
"repeater_neighbours": "Súsezný",
"neighbors_errorLoading": "Chyba pri načítaní susedov: {error}",
"neighbors_repeatersNeighbors": "Opakovadlá Súsezná",
"neighbors_repeatersNeighbours": "Opakovadlá Súsezná",
"neighbors_noData": "Nie je dostupná žiadna informácia o susedoch.",
"channels_createPrivateChannel": "Vytvorte súkromný kanál",
"channels_joinPrivateChannel": "Pripojiť sa k súkromnému kanálu",
@@ -1566,8 +1561,6 @@
"contacts_copyAdvertToClipboard": "Kopírovať reklamu do schránky",
"contacts_invalidAdvertFormat": "Neplatné kontaktné údaje",
"appSettings_languageRu": "Ruština",
"appSettings_enableMessageTracing": "Povoliť sledovanie správ",
"appSettings_enableMessageTracingSubtitle": "Zobraziť podrobné metadáta o smerovaní a časovaní správ",
"contacts_addContactFromClipboard": "Pridať kontakt z schránky",
"contacts_contactImported": "Kontakt bol importovaný.",
"contacts_zeroHopContactAdvertSent": "Poslal kontakt cez inzerát.",
@@ -1582,6 +1575,7 @@
"notification_newNodesCount": "{count} {count, plural, =1{nový uzol} few{nové uzly} other{nových uzlov}}",
"notification_newTypeDiscovered": "Nový {contactType} objavený",
"notification_receivedNewMessage": "Prijatá nová správa",
"contacts_ShareContact": "Kopírovať kontakt do schránky",
"settings_gpxExportRepeatersSubtitle": "Exportuje repeater / roomserver s lokalitou do súboru GPX.",
"settings_gpxExportContacts": "Export sprievodcov do GPX",
"settings_gpxExportSuccess": "Úspešne exportovaný súbor GPX.",
@@ -1597,296 +1591,6 @@
"settings_gpxExportChat": "Lokácie sprievodcov",
"settings_gpxExportShareText": "Mapové údaje exportované z meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open export dát GPX mapových údajov",
"pathTrace_someHopsNoLocation": "Jedna alebo viac chmeľov chýba lokalita!",
"pathTrace_clearTooltip": "Zmazať cestu",
"map_tapToAdd": "Kliknite na uzly, aby ste ich pridali k ceste.",
"map_removeLast": "Odstrániť posledný",
"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ý",
"snrIndicator_nearByRepeaters": "Miestne opakovače",
"chat_ShowAllPaths": "Zobraziť všetky cesty",
"settings_clientRepeat": "Opätovné použitie bez elektrickej siete",
"settings_clientRepeatFreqWarning": "Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.",
"settings_clientRepeatSubtitle": "Umožnite, aby toto zariadenie opakovávalo siete pre ostatných.",
"settings_aboutOpenMeteoAttribution": "Údaje o nadmorskej výške LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Jednotky",
"appSettings_unitsMetric": "Metrické (m / km)",
"appSettings_unitsImperial": "Imperiálne (ft / mi)",
"map_lineOfSight": "Line of Sight",
"map_losScreenTitle": "Line of Sight",
"losSelectStartEnd": "Vyberte počiatočný a koncový uzol pre LOS.",
"losRunFailed": "Kontrola priamej viditeľnosti zlyhala: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Vymazať všetky body",
"losRunToViewElevationProfile": "Ak chcete zobraziť výškový profil, spustite LOS",
"losMenuTitle": "Menu LOS",
"losMenuSubtitle": "Klepnutím na uzly alebo dlhým stlačením mapy získate vlastné body",
"losShowDisplayNodes": "Zobraziť uzly zobrazenia",
"losCustomPoints": "Vlastné body",
"losCustomPointLabel": "Vlastné {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Bod A",
"losPointB": "Bod B",
"losAntennaA": "Anténa A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Anténa B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Spustite LOS",
"losNoElevationData": "Žiadne údaje o nadmorskej výške",
"losProfileClear": "{distance} {distanceUnit}, vymazať LOS, min. vôľa {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, blokovaný {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: kontrolujem...",
"losStatusNoData": "LOS: žiadne údaje",
"losStatusSummary": "LOS: {clear}/{total} vymazané, {blocked} blokované, {unknown} neznáme",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Údaje o nadmorskej výške nie sú k dispozícii pre jednu alebo viacero vzoriek.",
"losErrorInvalidInput": "Neplatné body/údaje o nadmorskej výške pre výpočet LOS.",
"losRenameCustomPoint": "Premenovať vlastný bod",
"losPointName": "Názov bodu",
"losShowPanelTooltip": "Zobraziť panel LOS",
"losHidePanelTooltip": "Skryť panel LOS",
"losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Rádiový horizont",
"losLegendLosBeam": "Priama viditeľnosť",
"losLegendTerrain": "Terén",
"losFrequencyLabel": "Frekvencia",
"losFrequencyInfoTooltip": "Zobraziť podrobnosti výpočtu",
"losFrequencyDialogTitle": "Výpočet rádiového horizontu",
"losFrequencyDialogDescription": "Počnúc od k={baselineK} pri {baselineFreq} MHz výpočet upraví k-faktor pre aktuálne pásmo {frequencyMHz} MHz, ktorý definuje zakrivený strop rádiového horizontu.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_removeFromFavorites": "Odstrániť z označení",
"listFilter_addToFavorites": "Pridaj do obľúbených",
"listFilter_favorites": "Obľúbené",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_searchRoomServers": "Hľadaj {number}{str} serverov miestností...",
"contacts_searchFavorites": "Hľadať {number}{str} obľúbené...",
"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é",
"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"
"pathTrace_someHopsNoLocation": "Jedna alebo viac chmeľov chýba lokalita!"
}
+11 -307
View File
@@ -1,12 +1,4 @@
{
"channels_channelDeleteFailed": "Kanala {name} ni bilo mogoče izbrisati",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "sl",
"appTitle": "MeshCore Open",
"nav_contacts": "Stiki",
@@ -139,6 +131,9 @@
"settings_infoContactsCount": "Število stikov",
"settings_infoChannelCount": "Število kanalov",
"settings_presets": "Prednastavitve",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
"settings_preset433Mhz": "433 MHz",
"settings_frequency": "Frekvenca (MHz)",
"settings_frequencyHelper": "300,00 - 2500,00",
"settings_frequencyInvalid": "Neveljavna frekvenca (300-2500 MHz)",
@@ -148,6 +143,8 @@
"settings_txPower": "TX Moč (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Neveljavna TX moč (0-22 dBm)",
"settings_longRange": "DDolg doseg",
"settings_fastSpeed": "Visoka hitrost",
"settings_error": "Napaka: {message}",
"@settings_error": {
"placeholders": {
@@ -342,8 +339,6 @@
"channels_publicChannel": "Javni kanal",
"channels_privateChannel": "Zasebni kanal",
"channels_editChannel": "Uredi kanal",
"channels_muteChannel": "Utišaj kanal",
"channels_unmuteChannel": "Vklopi obvestila kanala",
"channels_deleteChannel": "Pošlji kanal",
"channels_deleteChannelConfirm": "Izbrišem \"{name}\"? To se ne da povrniti.",
"@channels_deleteChannelConfirm": {
@@ -1361,12 +1356,12 @@
}
}
},
"repeater_neighborsSubtitle": "Pogledati nič sosednjih hopjev.",
"repeater_neighbors": "Sosedi",
"repeater_neighboursSubtitle": "Pogledati nič sosednjih hopjev.",
"repeater_neighbours": "Sosedi",
"neighbors_receivedData": "Prejeto podatke o sosedih",
"neighbors_requestTimedOut": "Sosedi zahtevajo izklop po dogovoru.",
"neighbors_errorLoading": "Napaka pri obnašanju sosedov: {error}",
"neighbors_repeatersNeighbors": "Ponovitve Sosedi",
"neighbors_repeatersNeighbours": "Ponovitve Sosedi",
"neighbors_noData": "Niso na voljo podatki o sosedih.",
"channels_joinPrivateChannel": "Pridružite se zasebni skupini",
"channels_createPrivateChannelDesc": "Varno zaklenjeno s skrivnim ključem.",
@@ -1560,8 +1555,6 @@
"contacts_pathTraceTo": "Trace route to {name}",
"appSettings_languageRu": "Ruščina",
"appSettings_languageUk": "Ukrajinsko",
"appSettings_enableMessageTracing": "Omogoči sledenje sporočilom",
"appSettings_enableMessageTracingSubtitle": "Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil",
"contacts_contactImported": "Kontakt je bil uvožen.",
"contacts_contactImportFailed": "Kontakt ni bil uspešno uvožen.",
"contacts_zeroHopAdvert": "Reklama brez posrednikov",
@@ -1582,6 +1575,7 @@
"notification_newNodesCount": "{count} {count, plural, =1{novo vozlišče} =2{novi vozlišči} few{nova vozlišča} other{novih vozlišč}}",
"notification_newTypeDiscovered": "Odkrito novo {contactType}",
"notification_receivedNewMessage": "Prejeto novo sporočilo",
"contacts_ShareContact": "Kopiraj stik v Odložišče",
"settings_gpxExportAll": "Izvozi vse kontakte v GPX",
"settings_gpxExportContacts": "Izvoz spremljevalcev v GPX",
"settings_gpxExportRepeatersSubtitle": "Izvozi ponovljene oddajnike / strežnik sobe z lokacijo v datoteko GPX.",
@@ -1597,296 +1591,6 @@
"settings_gpxExportNoContacts": "Ni stikov za izvoz.",
"settings_gpxExportNotAvailable": "Ni podprto na vašem napravi/operacijskem sistemu",
"settings_gpxExportShareSubject": "meshcore-open izvoz podatkov GPX karte",
"pathTrace_someHopsNoLocation": "Ena ali več hmelju manjka lokacija!",
"map_tapToAdd": "Pritisnite na vozlišča, da jih dodate poti.",
"map_removeLast": "Odstrani Zadnji",
"map_runTrace": "Zaženi sledenje poti",
"pathTrace_clearTooltip": "Počisti pot",
"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",
"chat_ShowAllPaths": "Prikaži vse poti",
"settings_clientRepeatFreqWarning": "Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.",
"settings_clientRepeatSubtitle": "Omogočite temu naprave, da ponavlja paketne sporočila za druge.",
"settings_clientRepeat": "Neovadno ponavljanje",
"settings_aboutOpenMeteoAttribution": "Podatki o višini LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Enote",
"appSettings_unitsMetric": "Metrična (m/km)",
"appSettings_unitsImperial": "Imperialno (ft / mi)",
"map_lineOfSight": "Linija vida",
"map_losScreenTitle": "Linija vida",
"losSelectStartEnd": "Izberite začetno in končno vozlišče za LOS.",
"losRunFailed": "Preverjanje vidnega polja ni uspelo: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Počisti vse točke",
"losRunToViewElevationProfile": "Zaženite LOS za ogled višinskega profila",
"losMenuTitle": "LOS meni",
"losMenuSubtitle": "Tapnite vozlišča ali dolgo pritisnite na zemljevid za točke po meri",
"losShowDisplayNodes": "Pokaži prikazna vozlišča",
"losCustomPoints": "Točke po meri",
"losCustomPointLabel": "Po meri {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Točka A",
"losPointB": "Točka B",
"losAntennaA": "Antena A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antena B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Zaženi LOS",
"losNoElevationData": "Ni podatkov o višini",
"losProfileClear": "{distance} {distanceUnit}, čisti LOS, najmanjša razdalja {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, blokiral {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: preverjam ...",
"losStatusNoData": "LOS: ni podatkov",
"losStatusSummary": "LOS: {clear}/{total} jasno, {blocked} blokirano, {unknown} neznano",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Podatki o nadmorski višini niso na voljo za enega ali več vzorcev.",
"losErrorInvalidInput": "Neveljavni podatki o točkah/višini za izračun LOS.",
"losRenameCustomPoint": "Preimenujte točko po meri",
"losPointName": "Ime točke",
"losShowPanelTooltip": "Pokaži ploščo LOS",
"losHidePanelTooltip": "Skrij ploščo LOS",
"losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Radijski horizont",
"losLegendLosBeam": "Linija vidnosti",
"losLegendTerrain": "Teren",
"losFrequencyLabel": "Frekvenca",
"losFrequencyInfoTooltip": "Prikaži podrobnosti izračuna",
"losFrequencyDialogTitle": "Izračun radijskega horizonta",
"losFrequencyDialogDescription": "Začenši od k={baselineK} pri {baselineFreq} MHz, izračun prilagodi k-faktor za trenutni pas {frequencyMHz} MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_favorites": "Priljubljene",
"listFilter_removeFromFavorites": "Odstrani iz priljubljenih",
"listFilter_addToFavorites": "Dodaj v priljubljene",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_unread": "Neprebrano",
"contacts_searchFavorites": "Iskanje {number}{str} priljubljenih...",
"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...",
"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"
"pathTrace_someHopsNoLocation": "Ena ali več hmelju manjka lokacija!"
}
+11 -307
View File
@@ -1,12 +1,4 @@
{
"channels_channelDeleteFailed": "Det gick inte att ta bort kanalen \"{name}\"",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "sv",
"appTitle": "MeshCore Open",
"nav_contacts": "Kontakter",
@@ -139,6 +131,9 @@
"settings_infoContactsCount": "Kontakterantal",
"settings_infoChannelCount": "Kanalantal",
"settings_presets": "Fördefinierade inställningar",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
"settings_preset433Mhz": "433 MHz",
"settings_frequency": "Frekvens (MHz)",
"settings_frequencyHelper": "300,0 - 2500,0",
"settings_frequencyInvalid": "Ogiltig frekvens (300-2500 MHz)",
@@ -148,6 +143,8 @@
"settings_txPower": "TX-effekt (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Ogiltig TX-effekt (0-22 dBm)",
"settings_longRange": "Lång räckvidd",
"settings_fastSpeed": "Snabb hastighet",
"settings_error": "Fel: {message}",
"@settings_error": {
"placeholders": {
@@ -342,8 +339,6 @@
"channels_publicChannel": "Allmänt kanal",
"channels_privateChannel": "Privat kanal",
"channels_editChannel": "Redigera kanal",
"channels_muteChannel": "Tysta kanal",
"channels_unmuteChannel": "Slå på ljud för kanal",
"channels_deleteChannel": "Ta bort kanal",
"channels_deleteChannelConfirm": "Radera \"{name}\"? Detta kan inte ångras.",
"@channels_deleteChannelConfirm": {
@@ -1361,12 +1356,12 @@
}
}
},
"repeater_neighbors": "Grannar",
"repeater_neighborsSubtitle": "Visa noll hoppgrannar.",
"repeater_neighbours": "Grannar",
"repeater_neighboursSubtitle": "Visa noll hoppgrannar.",
"neighbors_receivedData": "Mottagna grannars data",
"neighbors_requestTimedOut": "Grannar begär tidsinställd utskick.",
"neighbors_errorLoading": "Fel vid inläsning av grannar: {error}",
"neighbors_repeatersNeighbors": "Upprepar grannar",
"neighbors_repeatersNeighbours": "Upprepar grannar",
"neighbors_noData": "Inga grannuppgifter finns tillgängliga.",
"channels_createPrivateChannel": "Skapa en privat kanal",
"channels_joinPrivateChannel": "Gå med i en Privat Kanal",
@@ -1566,8 +1561,6 @@
"contacts_copyAdvertToClipboard": "Kopiera annons till urklipp",
"contacts_invalidAdvertFormat": "Ogiltiga kontaktuppgifter",
"appSettings_languageUk": "Ukrainska",
"appSettings_enableMessageTracing": "Aktivera meddelandespårning",
"appSettings_enableMessageTracingSubtitle": "Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden",
"contacts_addContactFromClipboard": "Lägg till kontakt från urklipp",
"contacts_contactImported": "Kontakt har importerats.",
"contacts_zeroHopContactAdvertSent": "Skickat kontakt via annons.",
@@ -1582,6 +1575,7 @@
"notification_newNodesCount": "{count} {count, plural, =1{ny nod} other{nya noder}}",
"notification_newTypeDiscovered": "Ny {contactType} upptäckt",
"notification_receivedNewMessage": "Nytt meddelande mottaget",
"contacts_ShareContactZeroHop": "Dela kontakt via annons",
"settings_gpxExportAll": "Exportera alla kontakter till GPX",
"settings_gpxExportRepeatersSubtitle": "Exporterar repeater / roomserver med plats till GPX-fil.",
"settings_gpxExportSuccess": "Har exporterat GPX-fil med framgång",
@@ -1597,296 +1591,6 @@
"settings_gpxExportAllContacts": "Alla kontakters platser",
"settings_gpxExportShareSubject": "meshcore-open export av GPX-kartdata",
"settings_gpxExportShareText": "Kartdata exporterad från meshcore-open",
"pathTrace_someHopsNoLocation": "En eller flera av humlen saknar en plats!",
"pathTrace_clearTooltip": "Rensa väg",
"map_pathTraceCancelled": "Sökvägsspårning avbruten.",
"map_runTrace": "Kör spårsökning",
"map_tapToAdd": "Tryck på noder för att lägga till dem i banan.",
"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",
"chat_ShowAllPaths": "Visa alla vägar",
"settings_clientRepeatSubtitle": "Låt enheten repetera nätpaket för andra användare.",
"settings_clientRepeat": "Upprepa utan elnät",
"settings_clientRepeatFreqWarning": "För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz.",
"settings_aboutOpenMeteoAttribution": "LOS-höjddata: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Enheter",
"appSettings_unitsMetric": "Metriskt (m/km)",
"appSettings_unitsImperial": "Imperialt (ft / mi)",
"map_lineOfSight": "Synlinje",
"map_losScreenTitle": "Synlinje",
"losSelectStartEnd": "Välj start- och slutnoder för LOS.",
"losRunFailed": "Synlinjekontroll misslyckades: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Rensa alla punkter",
"losRunToViewElevationProfile": "Kör LOS för att se höjdprofil",
"losMenuTitle": "LOS-menyn",
"losMenuSubtitle": "Tryck på noder eller tryck länge på kartan för anpassade punkter",
"losShowDisplayNodes": "Visa displaynoder",
"losCustomPoints": "Anpassade poäng",
"losCustomPointLabel": "Anpassad {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Punkt A",
"losPointB": "Punkt B",
"losAntennaA": "Antenn A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antenn B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Kör LOS",
"losNoElevationData": "Inga höjddata",
"losProfileClear": "{distance} {distanceUnit}, rensa LOS, min clearance {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, blockerad av {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: kollar...",
"losStatusNoData": "LOS: inga data",
"losStatusSummary": "LOS: {clear}/{total} rensa, {blocked} blockerad, {unknown} okänd",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Höjddata är inte tillgänglig för ett eller flera prover.",
"losErrorInvalidInput": "Ogiltiga poäng/höjddata för LOS-beräkning.",
"losRenameCustomPoint": "Byt namn på anpassad punkt",
"losPointName": "Punktnamn",
"losShowPanelTooltip": "Visa LOS-panelen",
"losHidePanelTooltip": "Dölj LOS-panelen",
"losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Radiohorisont",
"losLegendLosBeam": "Siktlinje",
"losLegendTerrain": "Terräng",
"losFrequencyLabel": "Frekvens",
"losFrequencyInfoTooltip": "Visa detaljer om beräkningen",
"losFrequencyDialogTitle": "Beräkning av radiohorisonten",
"losFrequencyDialogDescription": "Med start från k={baselineK} vid {baselineFreq} MHz, justerar beräkningen k-faktorn för det aktuella {frequencyMHz} MHz-bandet, som definierar den böjda radiohorisonten.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_removeFromFavorites": "Ta bort från favoriter",
"listFilter_addToFavorites": "Lägg till i favoriter",
"listFilter_favorites": "Favoriter",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_unread": "Oläst",
"contacts_searchContactsNoNumber": "Sök kontakter...",
"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...",
"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"
"pathTrace_someHopsNoLocation": "En eller flera av humlen saknar en plats!"
}
+11 -307
View File
@@ -1,12 +1,4 @@
{
"channels_channelDeleteFailed": "Не вдалося видалити канал \"{name}\"",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "uk",
"appTitle": "MeshCore Open",
"nav_contacts": "Контакти",
@@ -139,6 +131,9 @@
"settings_infoContactsCount": "Кількість контактів",
"settings_infoChannelCount": "Кількість каналів",
"settings_presets": "Попередні налаштування",
"settings_preset915Mhz": "915 МГц",
"settings_preset868Mhz": "868 МГц",
"settings_preset433Mhz": "433 МГц",
"settings_frequency": "Частота (МГц)",
"settings_frequencyHelper": "300.0 - 2500.0",
"settings_frequencyInvalid": "Некоректна частота (300-2500 МГц)",
@@ -148,6 +143,8 @@
"settings_txPower": "Потужність TX (дБм)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Некоректна потужність TX (0-22 дБм)",
"settings_longRange": "Дальній діапазон",
"settings_fastSpeed": "Висока швидкість",
"settings_error": "Помилка: {message}",
"@settings_error": {
"placeholders": {
@@ -343,8 +340,6 @@
"channels_publicChannel": "Публічний канал",
"channels_privateChannel": "Приватний канал",
"channels_editChannel": "Редагувати канал",
"channels_muteChannel": "Вимкнути сповіщення каналу",
"channels_unmuteChannel": "Увімкнути сповіщення каналу",
"channels_deleteChannel": "Видалити канал",
"channels_deleteChannelConfirm": "Видалити {name}? Це не можна скасувати.",
"@channels_deleteChannelConfirm": {
@@ -1362,12 +1357,12 @@
}
}
},
"repeater_neighbors": "Сусіди",
"repeater_neighborsSubtitle": "Показати сусідів нульового стрибка.",
"repeater_neighbours": "Сусіди",
"repeater_neighboursSubtitle": "Показати сусідів нульового стрибка.",
"neighbors_receivedData": "Дані сусідів отримано",
"neighbors_requestTimedOut": "Час запиту сусідів вичерпано.",
"neighbors_errorLoading": "Помилка завантаження сусідів: {error}",
"neighbors_repeatersNeighbors": "Ретранслятори-сусіди",
"neighbors_repeatersNeighbours": "Ретранслятори-сусіди",
"neighbors_noData": "Дані про сусідів недоступні.",
"channels_createPrivateChannelDesc": "Захищено секретним ключем.",
"channels_joinPrivateChannel": "Приєднатися до приватного каналу",
@@ -1567,8 +1562,6 @@
"contacts_copyAdvertToClipboard": "Копіювати оголошення в буфер обміну",
"contacts_clipboardEmpty": "Буфер обміну порожній",
"appSettings_languageRu": "Російська",
"appSettings_enableMessageTracing": "Увімкнути відстеження повідомлень",
"appSettings_enableMessageTracingSubtitle": "Показувати детальні метадані про маршрутизацію та час для повідомлень",
"contacts_ShareContact": "Копіювати контакт у буфер обміну",
"contacts_zeroHopContactAdvertFailed": "Не вдалося надіслати контакт.",
"contacts_contactAdvertCopied": "Рекламу скопійовано до буфера обміну.",
@@ -1582,6 +1575,7 @@
"notification_newNodesCount": "{count} {count, plural, =1{новий вузол} few{нових вузли} many{нових вузлів} other{нових вузлів}}",
"notification_newTypeDiscovered": "Виявлено новий {contactType}",
"notification_receivedNewMessage": "Отримано нове повідомлення",
"contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням",
"settings_gpxExportRepeaters": "Експортувати ретранслятори / сервер кімнати до GPX",
"settings_gpxExportRepeatersSubtitle": "Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.",
"settings_gpxExportSuccess": "Успішно експортовано файл GPX.",
@@ -1597,296 +1591,6 @@
"settings_gpxExportShareText": "Дані карти експортовані з meshcore-open",
"settings_gpxExportAllContacts": "Усі місця контактів",
"settings_gpxExportShareSubject": "експорт даних карти meshcore-open у форматі GPX",
"pathTrace_someHopsNoLocation": "Одне або більше хмелів відсутнє місце розташування!",
"map_tapToAdd": "Натисніть на вузли, щоб додати їх до шляху",
"map_runTrace": "Виконати трасування шляху",
"pathTrace_clearTooltip": "Очистити шлях",
"map_removeLast": "Видалити останній",
"map_pathTraceCancelled": "Відмінується трасування шляху",
"scanner_enableBluetooth": "Увімкніть Bluetooth",
"scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.",
"scanner_chromeRequired": "Потрібен браузер Chrome",
"scanner_chromeRequiredMessage": "Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.",
"scanner_bluetoothOff": "Bluetooth вимкнено",
"snrIndicator_lastSeen": "Останній раз бачили",
"snrIndicator_nearByRepeaters": "Ближні ретранслятори",
"chat_ShowAllPaths": "Показати всі шляхи",
"settings_clientRepeatFreqWarning": "Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.",
"settings_clientRepeatSubtitle": "Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.",
"settings_clientRepeat": "Автономна система",
"settings_aboutOpenMeteoAttribution": "Дані про висоту LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "одиниці",
"appSettings_unitsMetric": "Метричний (м / км)",
"appSettings_unitsImperial": "Імперська (ft / mi)",
"map_lineOfSight": "Пряма видимість",
"map_losScreenTitle": "Пряма видимість",
"losSelectStartEnd": "Виберіть початковий і кінцевий вузли для LOS.",
"losRunFailed": "Помилка перевірки прямої видимості: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Очистити всі пункти",
"losRunToViewElevationProfile": "Запустіть LOS, щоб переглянути профіль висоти",
"losMenuTitle": "Меню LOS",
"losMenuSubtitle": "Торкніться вузлів або утримуйте карту, щоб отримати власні точки",
"losShowDisplayNodes": "Показати вузли відображення",
"losCustomPoints": "Користувальницькі точки",
"losCustomPointLabel": "Спеціальний {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Точка А",
"losPointB": "Точка Б",
"losAntennaA": "Антена A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Антена B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Запустіть LOS",
"losNoElevationData": "Немає даних про висоту",
"losProfileClear": "{distance} {distanceUnit}, чистий LOS, мінімальний зазор {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, заблоковано {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: перевірка...",
"losStatusNoData": "LOS: немає даних",
"losStatusSummary": "LOS: {clear}/{total} очищено, {blocked} заблоковано, {unknown} невідомо",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Дані про висоту недоступні для одного чи кількох зразків.",
"losErrorInvalidInput": "Недійсні дані про точки/висоту для розрахунку LOS.",
"losRenameCustomPoint": "Перейменуйте спеціальну точку",
"losPointName": "Назва точки",
"losShowPanelTooltip": "Показати панель LOS",
"losHidePanelTooltip": "Приховати панель LOS",
"losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Радіогоризонт",
"losLegendLosBeam": "Лінія прямої видимості",
"losLegendTerrain": "Рельєф",
"losFrequencyLabel": "Частота",
"losFrequencyInfoTooltip": "Переглянути деталі розрахунку",
"losFrequencyDialogTitle": "Розрахунок радіогоризонту",
"losFrequencyDialogDescription": "Починаючи з k={baselineK} на {baselineFreq} МГц, обчислення коригує k-фактор для поточного діапазону {frequencyMHz} МГц, який визначає викривлену межу радіогоризонту.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_removeFromFavorites": "Видалити зі списку улюблених",
"listFilter_addToFavorites": "Додати до улюблених",
"listFilter_favorites": "Улюблені",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_searchRoomServers": "Пошук {number}{str} серверів кімнат...",
"contacts_searchUsers": "Пошук {number}{str} користувачів...",
"contacts_searchFavorites": "Пошук {number}{str} улюблених...",
"contacts_searchContactsNoNumber": "Пошук контактів...",
"contacts_searchRepeaters": "Пошук {number}{str} ретрансляторів...",
"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": "Встановити моє місцезнаходження"
"pathTrace_someHopsNoLocation": "Одне або більше хмелів відсутнє місце розташування!"
}
+484 -785
View File
File diff suppressed because it is too large Load Diff
+7 -46
View File
@@ -1,12 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter/foundation.dart';
import 'package:meshcore_open/services/sparse_location_logger.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';
@@ -18,8 +15,6 @@ import 'services/ble_debug_log_service.dart';
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/timeout_prediction_service.dart';
import 'storage/prefs_manager.dart';
import 'utils/app_logger.dart';
@@ -39,9 +34,7 @@ void main() async {
final appDebugLogService = AppDebugLogService();
final backgroundService = BackgroundService();
final mapTileCacheService = MapTileCacheService();
final chatTextScaleService = ChatTextScaleService();
final timeoutPredictionService = TimeoutPredictionService(storage);
final sparseLocationLogger = SparseLocationLogger();
// Load settings
await appSettingsService.loadSettings();
@@ -55,10 +48,6 @@ void main() async {
final notificationService = NotificationService();
await notificationService.initialize();
await backgroundService.initialize();
_registerThirdPartyLicenses();
await chatTextScaleService.initialize();
await timeoutPredictionService.initialize();
// Wire up connector with services
connector.initialize(
@@ -68,7 +57,7 @@ void main() async {
bleDebugLogService: bleDebugLogService,
appDebugLogService: appDebugLogService,
backgroundService: backgroundService,
timeoutPredictionService: timeoutPredictionService,
sparseLocationLogger: sparseLocationLogger,
);
await connector.loadContactCache();
@@ -89,33 +78,11 @@ void main() async {
bleDebugLogService: bleDebugLogService,
appDebugLogService: appDebugLogService,
mapTileCacheService: mapTileCacheService,
chatTextScaleService: chatTextScaleService,
timeoutPredictionService: timeoutPredictionService,
sparseLocationLogger: sparseLocationLogger,
),
);
}
void _registerThirdPartyLicenses() {
LicenseRegistry.addLicense(() async* {
yield const LicenseEntryWithLineBreaks(
<String>['Open-Meteo Elevation API Data'],
'''
Data used by LOS elevation lookups is provided by Open-Meteo.
Open-Meteo terms and attribution:
https://open-meteo.com/en/terms
Elevation API:
https://open-meteo.com/en/docs/elevation-api
Attribution license reference:
Creative Commons Attribution 4.0 International (CC BY 4.0)
https://creativecommons.org/licenses/by/4.0/
''',
);
});
}
class MeshCoreApp extends StatelessWidget {
final MeshCoreConnector connector;
final MessageRetryService retryService;
@@ -125,8 +92,7 @@ class MeshCoreApp extends StatelessWidget {
final BleDebugLogService bleDebugLogService;
final AppDebugLogService appDebugLogService;
final MapTileCacheService mapTileCacheService;
final ChatTextScaleService chatTextScaleService;
final TimeoutPredictionService timeoutPredictionService;
final SparseLocationLogger sparseLocationLogger;
const MeshCoreApp({
super.key,
@@ -138,8 +104,7 @@ class MeshCoreApp extends StatelessWidget {
required this.bleDebugLogService,
required this.appDebugLogService,
required this.mapTileCacheService,
required this.chatTextScaleService,
required this.timeoutPredictionService,
required this.sparseLocationLogger,
});
@override
@@ -152,10 +117,8 @@ class MeshCoreApp extends StatelessWidget {
ChangeNotifierProvider.value(value: appSettingsService),
ChangeNotifierProvider.value(value: bleDebugLogService),
ChangeNotifierProvider.value(value: appDebugLogService),
ChangeNotifierProvider.value(value: chatTextScaleService),
Provider.value(value: storage),
Provider.value(value: mapTileCacheService),
ChangeNotifierProvider.value(value: timeoutPredictionService),
],
child: Consumer<AppSettingsService>(
builder: (context, settingsService, child) {
@@ -198,9 +161,7 @@ class MeshCoreApp extends StatelessWidget {
NotificationService().setLocale(locale);
return child ?? const SizedBox.shrink();
},
home: (PlatformInfo.isWeb && !PlatformInfo.isChrome)
? const ChromeRequiredScreen()
: const ScannerScreen(),
home: const ScannerScreen(),
);
},
),
+1 -72
View File
@@ -1,16 +1,3 @@
enum UnitSystem { metric, imperial }
extension UnitSystemValue on UnitSystem {
String get value {
switch (this) {
case UnitSystem.imperial:
return 'imperial';
case UnitSystem.metric:
return 'metric';
}
}
}
class AppSettings {
static const Object _unset = Object();
@@ -22,8 +9,6 @@ class AppSettings {
final bool mapKeyPrefixEnabled;
final String mapKeyPrefix;
final bool mapShowMarkers;
final bool mapShowGuessedLocations;
final bool enableMessageTracing;
final Map<String, double>? mapCacheBounds;
final int mapCacheMinZoom;
final int mapCacheMaxZoom;
@@ -36,10 +21,6 @@ class AppSettings {
final String? languageOverride; // null = system default
final bool appDebugLogEnabled;
final Map<String, String> batteryChemistryByDeviceId;
final Map<String, String> batteryChemistryByRepeaterId;
final UnitSystem unitSystem;
final Set<String> mutedChannels;
final bool mapShowDiscoveryContacts;
AppSettings({
this.clearPathOnMaxRetry = false,
@@ -50,8 +31,6 @@ class AppSettings {
this.mapKeyPrefixEnabled = false,
this.mapKeyPrefix = '',
this.mapShowMarkers = true,
this.mapShowGuessedLocations = true,
this.enableMessageTracing = false,
this.mapCacheBounds,
this.mapCacheMinZoom = 10,
this.mapCacheMaxZoom = 15,
@@ -64,13 +43,7 @@ class AppSettings {
this.languageOverride,
this.appDebugLogEnabled = false,
Map<String, String>? batteryChemistryByDeviceId,
Map<String, String>? batteryChemistryByRepeaterId,
this.unitSystem = UnitSystem.metric,
Set<String>? mutedChannels,
this.mapShowDiscoveryContacts = true,
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
mutedChannels = mutedChannels ?? {};
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {};
Map<String, dynamic> toJson() {
return {
@@ -82,8 +55,6 @@ class AppSettings {
'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,
'map_cache_max_zoom': mapCacheMaxZoom,
@@ -96,21 +67,10 @@ class AppSettings {
'language_override': languageOverride,
'app_debug_log_enabled': appDebugLogEnabled,
'battery_chemistry_by_device_id': batteryChemistryByDeviceId,
'battery_chemistry_by_repeater_id': batteryChemistryByRepeaterId,
'unit_system': unitSystem.value,
'muted_channels': mutedChannels.toList(),
'map_show_discovery_contacts': mapShowDiscoveryContacts,
};
}
factory AppSettings.fromJson(Map<String, dynamic> json) {
UnitSystem parseUnitSystem(dynamic value) {
if (value is String && value.toLowerCase() == 'imperial') {
return UnitSystem.imperial;
}
return UnitSystem.metric;
}
return AppSettings(
clearPathOnMaxRetry: json['clear_path_on_max_retry'] as bool? ?? false,
mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true,
@@ -121,9 +81,6 @@ class AppSettings {
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()),
),
@@ -144,19 +101,6 @@ class AppSettings {
(key, value) => MapEntry(key.toString(), value.toString()),
) ??
{},
batteryChemistryByRepeaterId:
(json['battery_chemistry_by_repeater_id'] as Map?)?.map(
(key, value) => MapEntry(key.toString(), value.toString()),
) ??
{},
unitSystem: parseUnitSystem(json['unit_system']),
mutedChannels:
((json['muted_channels'] as List?)
?.map((e) => e.toString())
.toSet()) ??
{},
mapShowDiscoveryContacts:
json['map_show_discovery_contacts'] as bool? ?? true,
);
}
@@ -169,8 +113,6 @@ class AppSettings {
bool? mapKeyPrefixEnabled,
String? mapKeyPrefix,
bool? mapShowMarkers,
bool? mapShowGuessedLocations,
bool? enableMessageTracing,
Object? mapCacheBounds = _unset,
int? mapCacheMinZoom,
int? mapCacheMaxZoom,
@@ -183,10 +125,6 @@ class AppSettings {
Object? languageOverride = _unset,
bool? appDebugLogEnabled,
Map<String, String>? batteryChemistryByDeviceId,
Map<String, String>? batteryChemistryByRepeaterId,
UnitSystem? unitSystem,
Set<String>? mutedChannels,
bool? mapShowDiscoveryContacts,
}) {
return AppSettings(
clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry,
@@ -197,9 +135,6 @@ class AppSettings {
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
: mapCacheBounds as Map<String, double>?,
@@ -219,12 +154,6 @@ class AppSettings {
appDebugLogEnabled: appDebugLogEnabled ?? this.appDebugLogEnabled,
batteryChemistryByDeviceId:
batteryChemistryByDeviceId ?? this.batteryChemistryByDeviceId,
batteryChemistryByRepeaterId:
batteryChemistryByRepeaterId ?? this.batteryChemistryByRepeaterId,
unitSystem: unitSystem ?? this.unitSystem,
mutedChannels: mutedChannels ?? this.mutedChannels,
mapShowDiscoveryContacts:
mapShowDiscoveryContacts ?? this.mapShowDiscoveryContacts,
);
}
}
+35 -57
View File
@@ -1,13 +1,10 @@
import 'dart:typed_data';
import 'package:meshcore_open/utils/app_logger.dart';
import '../connector/meshcore_protocol.dart';
class Contact {
final Uint8List publicKey;
final String name;
final int type;
final int flags;
final int pathLength; // -1 = flood, 0+ = direct hops (from device)
final Uint8List path; // Path bytes from device
final int?
@@ -17,14 +14,11 @@ class Contact {
final double? longitude;
final DateTime lastSeen;
final DateTime lastMessageAt;
final bool isActive;
final Uint8List? rawPacket;
Contact({
required this.publicKey,
required this.name,
required this.type,
this.flags = 0,
required this.pathLength,
required this.path,
this.pathOverride,
@@ -33,8 +27,6 @@ class Contact {
this.longitude,
required this.lastSeen,
DateTime? lastMessageAt,
this.isActive = true,
this.rawPacket,
}) : lastMessageAt = lastMessageAt ?? lastSeen;
String get publicKeyHex => pubKeyToHex(publicKey);
@@ -66,13 +58,11 @@ class Contact {
}
bool get hasLocation => latitude != null && longitude != null;
bool get isFavorite => (flags & contactFlagFavorite) != 0;
Contact copyWith({
Uint8List? publicKey,
String? name,
int? type,
int? flags,
int? pathLength,
Uint8List? path,
int? pathOverride,
@@ -82,14 +72,11 @@ class Contact {
double? longitude,
DateTime? lastSeen,
DateTime? lastMessageAt,
bool? isActive,
Uint8List? rawPacket,
}) {
return Contact(
publicKey: publicKey ?? this.publicKey,
name: name ?? this.name,
type: type ?? this.type,
flags: flags ?? this.flags,
pathLength: pathLength ?? this.pathLength,
path: path ?? this.path,
pathOverride: clearPathOverride
@@ -102,8 +89,6 @@ class Contact {
longitude: longitude ?? this.longitude,
lastSeen: lastSeen ?? this.lastSeen,
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
isActive: isActive ?? this.isActive,
rawPacket: rawPacket ?? this.rawPacket,
);
}
@@ -134,7 +119,7 @@ class Contact {
final pathBytes = _pathBytesForDisplay;
Uint8List? traceBytes;
if (pathBytes.isEmpty) {
if (pathLength <= 0) {
traceBytes = Uint8List(1);
traceBytes[0] = publicKey[0];
return traceBytes;
@@ -175,50 +160,43 @@ class Contact {
}
static Contact? fromFrame(Uint8List data) {
if (data.isEmpty) return null;
final reader = BufferReader(data);
try {
final respCode = reader.readByte();
if (respCode != respCodeContact && respCode != pushCodeNewAdvert) {
return null;
}
final pubKey = reader.readBytes(pubKeySize);
final type = reader.readByte();
final flags = reader.readByte();
final pathLen = reader.readByte();
final safePathLen = pathLen > 0
? (pathLen > maxPathSize ? maxPathSize : pathLen)
: 0;
final pathBytes = reader.readBytes(maxPathSize).sublist(0, safePathLen);
final name = reader.readCStringGreedy(maxNameSize);
if (data.length < contactFrameSize) return null;
if (data[0] != respCodeContact) return null;
final lastMod = reader.readUInt32LE();
final pubKey = Uint8List.fromList(
data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize),
);
final type = data[contactTypeOffset];
final pathLen = data[contactPathLenOffset].toSigned(8);
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);
double? lat, lon;
final latRaw = reader.readInt32LE();
final lonRaw = reader.readInt32LE();
if (latRaw != 0 || lonRaw != 0) {
lat = latRaw / 1e6;
lon = lonRaw / 1e6;
}
return Contact(
publicKey: pubKey,
name: name.isEmpty ? 'Unknown' : name,
type: type,
flags: flags,
pathLength: pathLen > 0 ? (pathLen > maxPathSize ? -1 : pathLen) : -1,
path: pathBytes,
latitude: lat,
longitude: lon,
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastMod * 1000),
isActive: true,
rawPacket: null,
);
} catch (e) {
appLogger.error('Failed to parse contact frame: $e');
return null;
double? lat, lon;
final latRaw = readInt32LE(data, contactLatOffset);
final lonRaw = readInt32LE(data, contactLonOffset);
if (latRaw != 0 || lonRaw != 0) {
lat = latRaw / 1e6;
lon = lonRaw / 1e6;
}
return Contact(
publicKey: pubKey,
name: name.isEmpty ? 'Unknown' : name,
type: type,
pathLength: pathLen,
path: pathBytes,
latitude: lat,
longitude: lon,
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000),
);
}
@override
-43
View File
@@ -1,43 +0,0 @@
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),
);
}
}
+40 -194
View File
@@ -59,200 +59,46 @@ class RadioSettings {
required this.txPowerDbm,
});
// Regional preset configurations
static final List<(String, RadioSettings)> presets = [
(
'Australia',
RadioSettings(
frequencyMHz: 915.8,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf10,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'Australia (Narrow)',
RadioSettings(
frequencyMHz: 916.575,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf7,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'Australia SA, WA, QLD',
RadioSettings(
frequencyMHz: 923.125,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf8,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'Czech Republic',
RadioSettings(
frequencyMHz: 869.432,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf7,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 14,
),
),
(
'EU 433MHz',
RadioSettings(
frequencyMHz: 433.650,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf11,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'EU/UK (Long Range)',
RadioSettings(
frequencyMHz: 869.525,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf11,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 14,
),
),
(
'EU/UK (Medium Range)',
RadioSettings(
frequencyMHz: 869.525,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf10,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 14,
),
),
(
'EU/UK (Narrow)',
RadioSettings(
frequencyMHz: 869.618,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf8,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 14,
),
),
(
'New Zealand',
RadioSettings(
frequencyMHz: 917.375,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf11,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'New Zealand (Narrow)',
RadioSettings(
frequencyMHz: 917.375,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf7,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'Portugal 433',
RadioSettings(
frequencyMHz: 433.375,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf9,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'Portugal 869',
RadioSettings(
frequencyMHz: 869.618,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf7,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 14,
),
),
(
'Switzerland',
RadioSettings(
frequencyMHz: 869.618,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf8,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 14,
),
),
(
'USA Arizona',
RadioSettings(
frequencyMHz: 908.205,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf10,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'USA/Canada',
RadioSettings(
frequencyMHz: 910.525,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf7,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'Vietnam',
RadioSettings(
frequencyMHz: 920.250,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf11,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
// Off-grid repeat presets (valid client_repeat frequencies)
(
'Off-Grid 433',
RadioSettings(
frequencyMHz: 433.0,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf11,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'Off-Grid 869',
RadioSettings(
frequencyMHz: 869.0,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf11,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 14,
),
),
(
'Off-Grid 918',
RadioSettings(
frequencyMHz: 918.0,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf11,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
];
// Preset configurations
static RadioSettings get preset915MHz => RadioSettings(
frequencyMHz: 915.0,
bandwidth: LoRaBandwidth.bw125,
spreadingFactor: LoRaSpreadingFactor.sf7,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
);
static RadioSettings get preset868MHz => RadioSettings(
frequencyMHz: 868.0,
bandwidth: LoRaBandwidth.bw125,
spreadingFactor: LoRaSpreadingFactor.sf7,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 14,
);
static RadioSettings get preset433MHz => RadioSettings(
frequencyMHz: 433.0,
bandwidth: LoRaBandwidth.bw125,
spreadingFactor: LoRaSpreadingFactor.sf7,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
);
static RadioSettings get presetLongRange => RadioSettings(
frequencyMHz: 915.0,
bandwidth: LoRaBandwidth.bw125,
spreadingFactor: LoRaSpreadingFactor.sf12,
codingRate: LoRaCodingRate.cr4_8,
txPowerDbm: 20,
);
static RadioSettings get presetFastSpeed => RadioSettings(
frequencyMHz: 915.0,
bandwidth: LoRaBandwidth.bw500,
spreadingFactor: LoRaSpreadingFactor.sf7,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
);
int get frequencyHz => (frequencyMHz * 1000).round();
int get bandwidthHz => bandwidth.hz;
+2 -3
View File
@@ -4,7 +4,6 @@ import 'package:provider/provider.dart';
import '../l10n/l10n.dart';
import '../services/app_debug_log_service.dart';
import '../widgets/adaptive_app_bar_title.dart';
class AppDebugLogScreen extends StatelessWidget {
const AppDebugLogScreen({super.key});
@@ -18,7 +17,7 @@ class AppDebugLogScreen extends StatelessWidget {
return Scaffold(
appBar: AppBar(
title: AdaptiveAppBarTitle(context.l10n.debugLog_appTitle),
title: Text(context.l10n.debugLog_appTitle),
centerTitle: true,
actions: [
IconButton(
@@ -56,7 +55,7 @@ class AppDebugLogScreen extends StatelessWidget {
child: hasEntries
? ListView.separated(
itemCount: entries.length,
separatorBuilder: (_, _) => const Divider(height: 1),
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final entry = entries[index];
return ListTile(
+1 -67
View File
@@ -3,10 +3,8 @@ import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../models/app_settings.dart';
import '../services/app_settings_service.dart';
import '../services/notification_service.dart';
import '../widgets/adaptive_app_bar_title.dart';
import 'map_cache_screen.dart';
class AppSettingsScreen extends StatelessWidget {
@@ -16,7 +14,7 @@ class AppSettingsScreen extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: AdaptiveAppBarTitle(context.l10n.appSettings_title),
title: Text(context.l10n.appSettings_title),
centerTitle: true,
),
body: SafeArea(
@@ -82,18 +80,6 @@ class AppSettingsScreen extends StatelessWidget {
trailing: const Icon(Icons.chevron_right),
onTap: () => _showLanguageDialog(context, settingsService),
),
const Divider(height: 1),
SwitchListTile(
secondary: const Icon(Icons.location_searching),
title: Text(context.l10n.appSettings_enableMessageTracing),
subtitle: Text(
context.l10n.appSettings_enableMessageTracingSubtitle,
),
value: settingsService.settings.enableMessageTracing,
onChanged: (value) {
settingsService.setEnableMessageTracing(value);
},
),
],
),
);
@@ -374,18 +360,6 @@ class AppSettingsScreen extends StatelessWidget {
onTap: () => _showTimeFilterDialog(context, settingsService),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.straighten),
title: Text(context.l10n.appSettings_unitsTitle),
subtitle: Text(
settingsService.settings.unitSystem == UnitSystem.imperial
? context.l10n.appSettings_unitsImperial
: context.l10n.appSettings_unitsMetric,
),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showUnitsDialog(context, settingsService),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.download_outlined),
title: Text(context.l10n.appSettings_offlineMapCache),
@@ -732,46 +706,6 @@ class AppSettingsScreen extends StatelessWidget {
);
}
void _showUnitsDialog(
BuildContext context,
AppSettingsService settingsService,
) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(context.l10n.appSettings_unitsTitle),
content: RadioGroup<UnitSystem>(
groupValue: settingsService.settings.unitSystem,
onChanged: (value) {
if (value != null) {
settingsService.setUnitSystem(value);
Navigator.pop(context);
}
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text(context.l10n.appSettings_unitsMetric),
leading: const Radio<UnitSystem>(value: UnitSystem.metric),
),
ListTile(
title: Text(context.l10n.appSettings_unitsImperial),
leading: const Radio<UnitSystem>(value: UnitSystem.imperial),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(context.l10n.common_close),
),
],
),
);
}
Widget _buildDebugCard(
BuildContext context,
AppSettingsService settingsService,
+2 -16
View File
@@ -4,7 +4,6 @@ import 'package:flutter/services.dart';
import '../l10n/l10n.dart';
import '../services/ble_debug_log_service.dart';
import '../connector/meshcore_protocol.dart';
import '../widgets/adaptive_app_bar_title.dart';
enum _BleLogView { frames, rawLogRx }
@@ -30,7 +29,7 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
: rawEntries.isNotEmpty;
return Scaffold(
appBar: AppBar(
title: AdaptiveAppBarTitle(context.l10n.debugLog_bleTitle),
title: Text(context.l10n.debugLog_bleTitle),
actions: [
IconButton(
tooltip: context.l10n.debugLog_copyLog,
@@ -101,7 +100,7 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
itemCount: showingFrames
? entries.length
: rawEntries.length,
separatorBuilder: (_, _) => const Divider(height: 1),
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
if (showingFrames) {
final entry = entries[index];
@@ -118,19 +117,6 @@ 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(''),
),
);
},
);
}
+168 -510
View File
@@ -1,5 +1,4 @@
import 'dart:convert';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
@@ -17,15 +16,11 @@ import '../helpers/utf8_length_limiter.dart';
import '../l10n/l10n.dart';
import '../models/channel.dart';
import '../models/channel_message.dart';
import '../services/app_settings_service.dart';
import '../services/chat_text_scale_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_status_icon.dart';
import 'channel_message_path_screen.dart';
import 'map_screen.dart';
@@ -221,50 +216,37 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
return Stack(
children: [
ChatZoomWrapper(
child: ListView.builder(
reverse: true, // List grows from bottom up
controller: _scrollController,
padding: const EdgeInsets.all(8),
itemCount: itemCount,
itemBuilder: (context, index) {
// Loading indicator now appears at end (bottom) of reversed list
if (_isLoadingOlder && index == itemCount - 1) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
),
ListView.builder(
reverse: true, // List grows from bottom up
controller: _scrollController,
padding: const EdgeInsets.all(8),
itemCount: itemCount,
itemBuilder: (context, index) {
// Loading indicator now appears at end (bottom) of reversed list
if (_isLoadingOlder && index == itemCount - 1) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
);
}
final messageIndex = index;
final message = reversedMessages[messageIndex];
if (!_messageKeys.containsKey(message.messageId)) {
_messageKeys[message.messageId] = GlobalKey();
}
return Container(
key: _messageKeys[message.messageId]!,
child: Builder(
builder: (context) {
final textScale = context
.select<ChatTextScaleService, double>(
(service) => service.scale,
);
return _buildMessageBubble(
message,
textScale,
);
},
),
);
},
),
}
final messageIndex = index;
final message = reversedMessages[messageIndex];
if (!_messageKeys.containsKey(message.messageId)) {
_messageKeys[message.messageId] = GlobalKey();
}
return Container(
key: _messageKeys[message.messageId]!,
child: _buildMessageBubble(message),
);
},
),
JumpToBottomButton(scrollController: _scrollController),
],
@@ -279,9 +261,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
);
}
Widget _buildMessageBubble(ChannelMessage message, double textScale) {
final settingsService = context.watch<AppSettingsService>();
final enableTracing = settingsService.settings.enableMessageTracing;
Widget _buildMessageBubble(ChannelMessage message) {
final isOutgoing = message.isOutgoing;
final gifId = _parseGifId(message.text);
final poi = _parsePoiMessage(message.text);
@@ -291,184 +271,107 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
? message.pathVariants.first
: Uint8List(0));
const maxSwipeOffset = 64.0;
const replySwipeThreshold = 64.0;
const bodyFontSize = 14.0;
final messageBody = Column(
crossAxisAlignment: isOutgoing
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: isOutgoing
? MainAxisAlignment.end
: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isOutgoing) ...[
_buildAvatar(message.senderName),
const SizedBox(width: 8),
],
Flexible(
child: GestureDetector(
onTap: () => _showMessagePathInfo(message),
onLongPress: () => _showMessageActions(message),
child: Container(
padding: gifId != null
? const EdgeInsets.all(4)
: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.65,
),
decoration: BoxDecoration(
color: isOutgoing
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isOutgoing) ...[
Padding(
padding: gifId != null
? const EdgeInsets.only(
left: 8,
top: 4,
bottom: 4,
)
: EdgeInsets.zero,
child: Text(
message.senderName,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: Column(
crossAxisAlignment: isOutgoing
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: isOutgoing
? MainAxisAlignment.end
: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isOutgoing) ...[
_buildAvatar(message.senderName),
const SizedBox(width: 8),
],
Flexible(
child: GestureDetector(
onTap: () => _showMessagePathInfo(message),
onLongPress: () => _showMessageActions(message),
child: Container(
padding: gifId != null
? const EdgeInsets.all(4)
: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.65,
),
decoration: BoxDecoration(
color: isOutgoing
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isOutgoing) ...[
Padding(
padding: gifId != null
? const EdgeInsets.only(
left: 8,
top: 4,
bottom: 4,
)
: EdgeInsets.zero,
child: Text(
message.senderName,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
),
),
if (gifId == null) const SizedBox(height: 4),
],
if (message.replyToMessageId != null) ...[
_buildReplyPreview(message, textScale),
const SizedBox(height: 8),
],
if (poi != null)
_buildPoiMessage(
context,
poi,
isOutgoing,
textScale,
trailing: (!enableTracing && isOutgoing)
? Padding(
padding: const EdgeInsets.only(bottom: 2),
child: MessageStatusIcon(
isAcked:
message.status ==
ChannelMessageStatus.sent &&
displayPath.isNotEmpty,
isFailed:
message.status ==
ChannelMessageStatus.failed,
),
)
: null,
)
else if (gifId != null)
Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: GifMessage(
url:
'https://media.giphy.com/media/$gifId/giphy.gif',
backgroundColor: Colors.transparent,
fallbackTextColor: isOutgoing
? Theme.of(context)
.colorScheme
.onPrimaryContainer
.withValues(alpha: 0.7)
: Theme.of(context).colorScheme.onSurface
.withValues(alpha: 0.6),
),
if (gifId == null) const SizedBox(height: 4),
],
if (message.replyToMessageId != null) ...[
_buildReplyPreview(message),
const SizedBox(height: 8),
],
if (poi != null)
_buildPoiMessage(context, poi, isOutgoing)
else if (gifId != null)
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: GifMessage(
url:
'https://media.giphy.com/media/$gifId/giphy.gif',
backgroundColor: Colors.transparent,
fallbackTextColor: isOutgoing
? Theme.of(context)
.colorScheme
.onPrimaryContainer
.withValues(alpha: 0.7)
: Theme.of(context).colorScheme.onSurface
.withValues(alpha: 0.6),
),
if (!enableTracing && isOutgoing)
Positioned(
top: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: isOutgoing
? Theme.of(
context,
).colorScheme.primaryContainer
: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(10),
topRight: Radius.circular(8),
),
),
child: MessageStatusIcon(
isAcked:
message.status ==
ChannelMessageStatus.sent &&
displayPath.isNotEmpty,
isFailed:
message.status ==
ChannelMessageStatus.failed,
),
),
),
],
)
else
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(
child: Linkify(
text: message.text,
style: TextStyle(
fontSize: bodyFontSize * textScale,
),
linkStyle: 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,
),
),
)
else
Linkify(
text: message.text,
style: const TextStyle(fontSize: 14),
linkStyle: const TextStyle(
fontSize: 14,
color: Colors.green,
decoration: TextDecoration.underline,
),
if (!enableTracing && isOutgoing) ...[
const SizedBox(width: 4),
Padding(
padding: const EdgeInsets.only(bottom: 2),
child: MessageStatusIcon(
isAcked:
message.status ==
ChannelMessageStatus.sent &&
displayPath.isNotEmpty,
isFailed:
message.status ==
ChannelMessageStatus.failed,
),
),
],
],
),
if (enableTracing) ...[
options: const LinkifyOptions(
humanize: false,
defaultToHttps: false,
),
linkifiers: const [UrlLinkifier()],
onOpen: (link) =>
LinkHandler.handleLinkTap(context, link.url),
),
if (displayPath.isNotEmpty) ...[
const SizedBox(height: 4),
Padding(
@@ -540,81 +443,25 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
),
),
],
],
),
),
),
),
],
),
if (message.reactions.isNotEmpty) ...[
const SizedBox(height: 4),
Padding(
padding: EdgeInsets.only(left: isOutgoing ? 0 : 48),
child: _buildReactionsDisplay(message),
),
],
),
if (message.reactions.isNotEmpty) ...[
const SizedBox(height: 4),
Padding(
padding: EdgeInsets.only(left: isOutgoing ? 0 : 48),
child: _buildReactionsDisplay(message),
),
],
],
);
if (!isOutgoing) {
return _SwipeReplyBubble(
maxSwipeOffset: maxSwipeOffset,
replySwipeThreshold: replySwipeThreshold,
onReplyTriggered: () => _setReplyingTo(message),
hintBuilder: ({required isStart}) =>
_buildReplySwipeHint(isStart: isStart),
child: messageBody,
);
} else {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: messageBody,
);
}
}
Widget _buildReplySwipeHint({required bool isStart}) {
final colorScheme = Theme.of(context).colorScheme;
final content = Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.reply, color: colorScheme.primary),
const SizedBox(width: 6),
Text(
context.l10n.chat_reply,
style: TextStyle(
color: colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
],
);
return Container(
alignment: isStart ? Alignment.centerLeft : Alignment.centerRight,
padding: const EdgeInsets.symmetric(horizontal: 16),
color: colorScheme.primary.withValues(alpha: 0.08),
child: isStart
? content
: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
context.l10n.chat_reply,
style: TextStyle(
color: colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 6),
Icon(Icons.reply, color: colorScheme.primary),
],
),
),
);
}
Widget _buildReplyPreview(ChannelMessage message, double textScale) {
Widget _buildReplyPreview(ChannelMessage message) {
final connector = context.read<MeshCoreConnector>();
final isOwnNode = message.replyToSenderName == connector.selfName;
final replyText = message.replyToText ?? '';
@@ -642,7 +489,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
const SizedBox(width: 4),
Text(
context.l10n.chat_location,
style: TextStyle(fontSize: 12 * textScale, color: previewTextColor),
style: TextStyle(fontSize: 12, color: previewTextColor),
),
],
);
@@ -652,7 +499,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12 * textScale,
fontSize: 12,
color: previewTextColor,
fontStyle: FontStyle.italic,
),
@@ -676,7 +523,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
Text(
context.l10n.chat_replyTo(message.replyToSenderName ?? ''),
style: TextStyle(
fontSize: 11 * textScale,
fontSize: 11,
fontWeight: FontWeight.bold,
color: isOwnNode
? Theme.of(context).colorScheme.primary
@@ -752,13 +599,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
return _PoiInfo(lat: lat, lon: lon, label: label);
}
Widget _buildPoiMessage(
BuildContext context,
_PoiInfo poi,
bool isOutgoing,
double textScale, {
Widget? trailing,
}) {
Widget _buildPoiMessage(BuildContext context, _PoiInfo poi, bool isOutgoing) {
final colorScheme = Theme.of(context).colorScheme;
final textColor = isOutgoing
? colorScheme.onPrimaryContainer
@@ -794,21 +635,16 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
children: [
Text(
context.l10n.chat_poiShared,
style: TextStyle(
color: textColor,
fontWeight: FontWeight.w600,
fontSize: 14 * textScale,
),
style: TextStyle(color: textColor, fontWeight: FontWeight.w600),
),
if (poi.label.isNotEmpty)
Text(
poi.label,
style: TextStyle(color: metaColor, fontSize: 12 * textScale),
style: TextStyle(color: metaColor, fontSize: 12),
),
],
),
),
if (trailing != null) ...[const SizedBox(width: 4), trailing],
],
);
}
@@ -873,7 +709,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
return colors[hash.abs() % colors.length];
}
Widget _buildReplyBanner(double textScale) {
Widget _buildReplyBanner() {
final message = _replyingToMessage!;
return Container(
width: double.infinity,
@@ -899,7 +735,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
Text(
context.l10n.chat_replyingTo(message.senderName),
style: TextStyle(
fontSize: 12 * textScale,
fontSize: 12,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
@@ -909,7 +745,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 11 * textScale,
fontSize: 11,
color: Theme.of(
context,
).colorScheme.onSecondaryContainer.withValues(alpha: 0.7),
@@ -936,15 +772,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_replyingToMessage != null)
Builder(
builder: (context) {
final textScale = context.select<ChatTextScaleService, double>(
(service) => service.scale,
);
return _buildReplyBanner(textScale);
},
),
if (_replyingToMessage != null) _buildReplyBanner(),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
@@ -970,47 +798,30 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
builder: (context, value, child) {
final gifId = _parseGifId(value.text);
if (gifId != null) {
return Focus(
autofocus: true,
onKeyEvent: (node, event) {
if (event is KeyDownEvent &&
(event.logicalKey == LogicalKeyboardKey.enter ||
event.logicalKey ==
LogicalKeyboardKey.numpadEnter)) {
_sendMessage();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
child: Row(
children: [
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: GifMessage(
url:
'https://media.giphy.com/media/$gifId/giphy.gif',
backgroundColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
fallbackTextColor: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.6),
maxSize: 160,
),
return Row(
children: [
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: GifMessage(
url:
'https://media.giphy.com/media/$gifId/giphy.gif',
backgroundColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
fallbackTextColor: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.6),
maxSize: 160,
),
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.close),
onPressed: () {
_textController.clear();
_textFieldFocusNode.requestFocus();
},
),
],
),
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => _textController.clear(),
),
],
);
}
@@ -1073,7 +884,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
connector.sendChannelMessage(widget.channel, messageText);
_textController.clear();
_cancelReply();
_textFieldFocusNode.requestFocus();
}
String _formatTime(DateTime time) {
@@ -1091,8 +901,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ChannelMessagePathScreen(message: message, channelMessage: true),
builder: (context) => ChannelMessagePathScreen(message: message),
),
);
}
@@ -1197,157 +1006,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
}
}
class _SwipeReplyBubble extends StatefulWidget {
final double maxSwipeOffset;
final double replySwipeThreshold;
final VoidCallback onReplyTriggered;
final Widget Function({required bool isStart}) hintBuilder;
final Widget child;
const _SwipeReplyBubble({
required this.maxSwipeOffset,
required this.replySwipeThreshold,
required this.onReplyTriggered,
required this.hintBuilder,
required this.child,
});
@override
State<_SwipeReplyBubble> createState() => _SwipeReplyBubbleState();
}
class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> {
Offset? _swipeStartPosition;
double _swipeOffset = 0;
double _maxSwipeDistance = 0;
int? _swipePointerId;
bool _swipeLockedToHorizontal = false;
void _handleSwipeStart(Offset position) {
_swipeStartPosition = position;
_maxSwipeDistance = 0;
if (_swipeOffset != 0) {
setState(() => _swipeOffset = 0);
}
}
void _handleSwipePointerDown(PointerDownEvent event) {
_swipePointerId = event.pointer;
_swipeLockedToHorizontal = false;
_handleSwipeStart(event.position);
}
void _handleSwipePointerMove(PointerMoveEvent event) {
if (_swipePointerId != event.pointer || _swipeStartPosition == null) {
return;
}
final dx = event.position.dx - _swipeStartPosition!.dx;
const axisLockThreshold = 12.0;
if (!_swipeLockedToHorizontal) {
if (-dx < axisLockThreshold) {
return;
}
_swipeLockedToHorizontal = true;
}
_handleSwipeUpdate(event.position);
}
void _handleSwipeUpdate(Offset position) {
if (_swipeStartPosition == null) return;
final dx = position.dx - _swipeStartPosition!.dx;
if (dx >= 0) return;
if (-dx < 6) return;
if (-dx > _maxSwipeDistance) {
_maxSwipeDistance = -dx;
}
final double clamped = dx.clamp(-widget.maxSwipeOffset, 0.0).toDouble();
final adjusted = _applySwipeResistance(clamped, widget.maxSwipeOffset);
if (adjusted != _swipeOffset) {
setState(() => _swipeOffset = adjusted);
}
}
void _handleSwipePointerUp(Offset position) {
if (_swipeLockedToHorizontal && _swipeStartPosition != null) {
final dx = position.dx - _swipeStartPosition!.dx;
final peak = math.max(
_maxSwipeDistance,
(-dx).clamp(0.0, double.infinity),
);
if (peak >= widget.replySwipeThreshold) {
widget.onReplyTriggered();
HapticFeedback.selectionClick();
}
}
_resetSwipe();
}
void _resetSwipe() {
if (_swipeOffset != 0) {
setState(() => _swipeOffset = 0);
}
_swipeStartPosition = null;
_maxSwipeDistance = 0;
_swipePointerId = null;
_swipeLockedToHorizontal = false;
}
double _applySwipeResistance(double rawOffset, double maxOffset) {
final abs = rawOffset.abs();
if (abs <= 0) return 0;
final norm = (abs / maxOffset).clamp(0.0, 1.0);
const deadZone = 0.18;
if (norm <= deadZone) {
return rawOffset.sign * maxOffset * (norm * 0.08);
}
final t = ((norm - deadZone) / (1 - deadZone)).clamp(0.0, 1.0);
final curved = t < 0.5
? 16 * math.pow(t, 5)
: 1 - math.pow(-2 * t + 2, 5) / 2;
const deadZoneEnd = 0.0144;
return rawOffset.sign *
maxOffset *
(deadZoneEnd + curved * (1 - deadZoneEnd));
}
@override
Widget build(BuildContext context) {
return Listener(
onPointerDown: _handleSwipePointerDown,
onPointerMove: _handleSwipePointerMove,
onPointerUp: (event) => _handleSwipePointerUp(event.position),
onPointerCancel: (_) => _resetSwipe(),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: Stack(
alignment: Alignment.center,
children: [
Positioned.fill(
child: Opacity(
opacity: _swipeOffset.abs() / widget.maxSwipeOffset,
child: widget.hintBuilder(isStart: false),
),
),
AnimatedContainer(
duration: const Duration(milliseconds: 150),
transform: Matrix4.translationValues(_swipeOffset, 0, 0),
curve: Curves.easeOut,
child: widget.child,
),
],
),
),
);
}
}
class _PoiInfo {
final double lat;
final double lon;
+59 -194
View File
@@ -9,42 +9,27 @@ import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../services/map_tile_cache_service.dart';
import '../services/app_settings_service.dart';
import '../connector/meshcore_protocol.dart';
import '../l10n/app_localizations.dart';
import '../l10n/l10n.dart';
import '../models/channel_message.dart';
import '../models/app_settings.dart';
import '../models/contact.dart';
import '../widgets/adaptive_app_bar_title.dart';
class ChannelMessagePathScreen extends StatelessWidget {
final ChannelMessage message;
final bool channelMessage;
const ChannelMessagePathScreen({
super.key,
required this.message,
this.channelMessage = false,
});
const ChannelMessagePathScreen({super.key, required this.message});
@override
Widget build(BuildContext context) {
return Consumer<MeshCoreConnector>(
builder: (context, connector, _) {
final l10n = context.l10n;
final primaryPathTmp = _selectPrimaryPath(
final primaryPath = _selectPrimaryPath(
message.pathBytes,
message.pathVariants,
);
final primaryPath = !channelMessage && !message.isOutgoing
? Uint8List.fromList(primaryPathTmp.reversed.toList())
: primaryPathTmp;
final contacts = <Contact>[
...connector.contacts,
...connector.discoveredContacts,
];
final hops = _buildPathHops(primaryPath, contacts, l10n);
final hops = _buildPathHops(primaryPath, connector.contacts, l10n);
final hasHopDetails = primaryPath.isNotEmpty;
final observedLabel = _formatObservedHops(
primaryPath.length,
@@ -52,9 +37,10 @@ class ChannelMessagePathScreen extends StatelessWidget {
l10n,
);
final extraPaths = _otherPaths(primaryPath, message.pathVariants);
return Scaffold(
appBar: AppBar(
title: AdaptiveAppBarTitle(l10n.channelPath_title),
title: Text(l10n.channelPath_title),
actions: [
IconButton(
icon: const Icon(Icons.radar_outlined),
@@ -64,9 +50,9 @@ class ChannelMessagePathScreen extends StatelessWidget {
MaterialPageRoute(
builder: (context) => PathTraceMapScreen(
title: context.l10n.contacts_repeaterPathTrace,
path: primaryPath,
path: Uint8List.fromList(primaryPath),
flipPathRound: true,
reversePathRound: !message.isOutgoing && !channelMessage,
reversePathRound: true,
),
),
),
@@ -76,7 +62,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
tooltip: l10n.channelPath_viewMap,
onPressed: hasHopDetails
? () {
_openPathMap(context, channelMessage: channelMessage);
_openPathMap(context);
}
: null,
),
@@ -171,11 +157,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
),
subtitle: Text(_formatPathPrefixes(variants[i])),
trailing: const Icon(Icons.map_outlined, size: 20),
onTap: () => _openPathMap(
context,
initialPath: variants[i],
channelMessage: channelMessage,
),
onTap: () => _openPathMap(context, initialPath: variants[i]),
),
),
],
@@ -266,18 +248,13 @@ class ChannelMessagePathScreen extends StatelessWidget {
);
}
void _openPathMap(
BuildContext context, {
Uint8List? initialPath,
bool channelMessage = false,
}) {
void _openPathMap(BuildContext context, {Uint8List? initialPath}) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChannelMessagePathMapScreen(
message: message,
initialPath: initialPath,
channelMessage: channelMessage,
),
),
);
@@ -287,13 +264,11 @@ class ChannelMessagePathScreen extends StatelessWidget {
class ChannelMessagePathMapScreen extends StatefulWidget {
final ChannelMessage message;
final Uint8List? initialPath;
final bool channelMessage;
const ChannelMessagePathMapScreen({
super.key,
required this.message,
this.initialPath,
this.channelMessage = false,
});
@override
@@ -303,12 +278,8 @@ class ChannelMessagePathMapScreen extends StatefulWidget {
class _ChannelMessagePathMapScreenState
extends State<ChannelMessagePathMapScreen> {
static const double _labelZoomThreshold = 8.5;
Uint8List? _selectedPath;
double _pathDistance = 0.0;
bool _showNodeLabels = true;
bool _didReceivePositionUpdate = false;
@override
void initState() {
@@ -343,8 +314,6 @@ class _ChannelMessagePathMapScreenState
Widget build(BuildContext context) {
return Consumer<MeshCoreConnector>(
builder: (context, connector, _) {
final settings = context.watch<AppSettingsService>().settings;
final isImperial = settings.unitSystem == UnitSystem.imperial;
final tileCache = context.read<MapTileCacheService>();
final primaryPath = _selectPrimaryPath(
widget.message.pathBytes,
@@ -354,42 +323,25 @@ class _ChannelMessagePathMapScreenState
primaryPath,
widget.message.pathVariants,
);
final selectedPathTmp = _resolveSelectedPath(
final selectedPath = _resolveSelectedPath(
_selectedPath,
observedPaths,
primaryPath,
);
final selectedPath =
((!widget.message.isOutgoing && !widget.channelMessage) ||
(widget.message.isOutgoing && widget.channelMessage))
? Uint8List.fromList(selectedPathTmp.reversed.toList())
: selectedPathTmp;
final selectedIndex = _indexForPath(selectedPath, observedPaths);
final contacts = <Contact>[
...connector.contacts,
...connector.discoveredContacts,
];
final hops = _buildPathHops(selectedPath, contacts, context.l10n);
final hops = _buildPathHops(
selectedPath,
connector.contacts,
context.l10n,
);
final points = <LatLng>[];
if ((widget.message.isOutgoing && !widget.channelMessage) ||
(widget.message.isOutgoing && widget.channelMessage)) {
points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
}
for (final hop in hops) {
if (hop.hasLocation) {
points.add(hop.position!);
}
}
if ((!widget.message.isOutgoing && !widget.channelMessage) ||
(!widget.message.isOutgoing && widget.channelMessage)) {
points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
}
points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
final polylines = points.length > 1
? [
@@ -405,9 +357,6 @@ class _ChannelMessagePathMapScreenState
? points.first
: const LatLng(0, 0);
final initialZoom = points.isNotEmpty ? 13.0 : 2.0;
if (!_didReceivePositionUpdate) {
_showNodeLabels = initialZoom >= _labelZoomThreshold;
}
final bounds = points.length > 1
? LatLngBounds.fromPoints(points)
: null;
@@ -417,9 +366,7 @@ class _ChannelMessagePathMapScreenState
_pathDistance = _getPathDistance(points);
return Scaffold(
appBar: AppBar(
title: AdaptiveAppBarTitle(context.l10n.channelPath_mapTitle),
),
appBar: AppBar(title: Text(context.l10n.channelPath_mapTitle)),
body: SafeArea(
top: false,
child: Stack(
@@ -441,17 +388,6 @@ class _ChannelMessagePathMapScreenState
interactionOptions: InteractionOptions(
flags: ~InteractiveFlag.rotate,
),
onPositionChanged: (camera, hasGesture) {
final shouldShow = camera.zoom >= _labelZoomThreshold;
if (!_didReceivePositionUpdate ||
shouldShow != _showNodeLabels) {
if (!mounted) return;
setState(() {
_didReceivePositionUpdate = true;
_showNodeLabels = shouldShow;
});
}
},
),
children: [
TileLayer(
@@ -463,12 +399,7 @@ class _ChannelMessagePathMapScreenState
),
if (polylines.isNotEmpty)
PolylineLayer(polylines: polylines),
MarkerLayer(
markers: _buildHopMarkers(
hops,
showLabels: _showNodeLabels,
),
),
MarkerLayer(markers: _buildHopMarkers(hops)),
],
),
if (observedPaths.length > 1)
@@ -491,7 +422,7 @@ class _ChannelMessagePathMapScreenState
),
),
),
_buildLegendCard(context, hops, isImperial),
_buildLegendCard(context, hops),
],
),
),
@@ -563,61 +494,45 @@ class _ChannelMessagePathMapScreenState
);
}
List<Marker> _buildHopMarkers(
List<_PathHop> hops, {
required bool showLabels,
}) {
final markers = <Marker>[];
for (final hop in hops) {
if (!hop.hasLocation) continue;
final point = hop.position!;
markers.add(
Marker(
point: point,
width: 35,
height: 35,
child: Container(
decoration: BoxDecoration(
color: Colors.green,
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),
List<Marker> _buildHopMarkers(List<_PathHop> hops) {
return [
for (final hop in hops)
if (hop.hasLocation)
Marker(
point: hop.position!,
width: 35,
height: 35,
child: Container(
decoration: BoxDecoration(
color: Colors.green,
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: Text(
hop.index.toString(),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
],
),
alignment: Alignment.center,
child: Text(
hop.index.toString(),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
),
);
if (showLabels) {
markers.add(
_buildNodeLabelMarker(
point: point,
label: hop.contact?.name ?? _formatPrefix(hop.prefix),
),
);
}
}
final selfLat = context.read<MeshCoreConnector>().selfLatitude;
final selfLon = context.read<MeshCoreConnector>().selfLongitude;
if (selfLat != null && selfLon != null) {
final selfPoint = LatLng(selfLat, selfLon);
markers.add(
if (context.read<MeshCoreConnector>().selfLatitude != null &&
context.read<MeshCoreConnector>().selfLongitude != null)
Marker(
point: selfPoint,
point: LatLng(
context.read<MeshCoreConnector>().selfLatitude!,
context.read<MeshCoreConnector>().selfLongitude!,
),
width: 35,
height: 35,
child: Container(
@@ -644,60 +559,10 @@ class _ChannelMessagePathMapScreenState
),
),
),
);
if (showLabels) {
markers.add(
_buildNodeLabelMarker(
point: selfPoint,
label: context.l10n.pathTrace_you,
),
);
}
}
return markers;
];
}
Marker _buildNodeLabelMarker({required LatLng point, required String label}) {
return Marker(
point: point,
width: 120,
height: 24,
alignment: Alignment.topCenter,
child: IgnorePointer(
child: Transform.translate(
offset: const Offset(0, -20),
child: FittedBox(
fit: BoxFit.contain,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
),
),
),
),
);
}
Widget _buildLegendCard(
BuildContext context,
List<_PathHop> hops,
bool isImperial,
) {
Widget _buildLegendCard(BuildContext context, List<_PathHop> hops) {
final l10n = context.l10n;
final maxHeight = MediaQuery.of(context).size.height * 0.35;
final estimatedHeight = 72.0 + (hops.length * 56.0);
@@ -716,7 +581,7 @@ class _ChannelMessagePathMapScreenState
Padding(
padding: const EdgeInsets.all(12),
child: Text(
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistance, isImperial: isImperial)}',
'${l10n.channelPath_repeaterHops} (${(_pathDistance / 1609.34).toStringAsFixed(2)} Miles / ${(_pathDistance / 1000).toStringAsFixed(2)} Km)',
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
@@ -729,7 +594,7 @@ class _ChannelMessagePathMapScreenState
: ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 4),
itemCount: hops.length,
separatorBuilder: (_, _) => const Divider(height: 1),
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final hop = hops[index];
return ListTile(
+37 -134
View File
@@ -3,20 +3,18 @@ import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:meshcore_open/storage/channel_message_store.dart';
import 'package:meshcore_open/widgets/app_bar.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../services/app_settings_service.dart';
import '../models/channel.dart';
import '../models/community.dart';
import '../storage/community_store.dart';
import '../utils/dialog_utils.dart';
import '../utils/disconnect_navigation_mixin.dart';
import '../utils/route_transitions.dart';
import '../widgets/battery_indicator.dart';
import '../widgets/list_filter_widget.dart';
import '../widgets/empty_state.dart';
import '../widgets/qr_code_display.dart';
@@ -51,8 +49,6 @@ class _ChannelsScreenState extends State<ChannelsScreen>
// Cache of PSK hex -> Community for quick lookup
final Map<String, Community> _pskToCommunity = {};
ChannelMessageStore get _channelMessageStore => ChannelMessageStore();
@override
void initState() {
super.initState();
@@ -63,8 +59,6 @@ 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(() {
@@ -111,9 +105,6 @@ class _ChannelsScreenState extends State<ChannelsScreen>
Widget build(BuildContext context) {
final connector = context.watch<MeshCoreConnector>();
final channelMessageStore = ChannelMessageStore();
channelMessageStore.setPublicKeyHex = connector.selfPublicKeyHex;
// Auto-navigate back to scanner if disconnected
if (!checkConnectionAndNavigate(connector)) {
return const SizedBox.shrink();
@@ -125,7 +116,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
canPop: allowBack,
child: Scaffold(
appBar: AppBar(
title: AppBarTitle(context.l10n.channels_title),
leading: BatteryIndicator(connector: connector),
title: Text(context.l10n.channels_title),
centerTitle: true,
automaticallyImplyLeading: false,
actions: [
@@ -312,7 +304,6 @@ class _ChannelsScreenState extends State<ChannelsScreen>
return _buildChannelTile(
context,
connector,
channelMessageStore,
channel,
showDragHandle: true,
dragIndex: index,
@@ -332,7 +323,6 @@ class _ChannelsScreenState extends State<ChannelsScreen>
return _buildChannelTile(
context,
connector,
channelMessageStore,
channel,
);
},
@@ -362,7 +352,6 @@ class _ChannelsScreenState extends State<ChannelsScreen>
Widget _buildChannelTile(
BuildContext context,
MeshCoreConnector connector,
ChannelMessageStore channelMessageStore,
Channel channel, {
bool showDragHandle = false,
int? dragIndex,
@@ -479,12 +468,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
);
}
},
onLongPress: () => _showChannelActions(
context,
connector,
channelMessageStore,
channel,
),
onLongPress: () => _showChannelActions(context, connector, channel),
),
);
}
@@ -492,16 +476,11 @@ class _ChannelsScreenState extends State<ChannelsScreen>
void _showChannelActions(
BuildContext context,
MeshCoreConnector connector,
ChannelMessageStore channelMessageStore,
Channel channel,
) {
final parentContext = context;
final settingsService = context.read<AppSettingsService>();
final isMuted = settingsService.isChannelMuted(channel.name);
showModalBottomSheet(
context: parentContext,
builder: (sheetContext) => SafeArea(
context: context,
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -509,30 +488,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
leading: const Icon(Icons.edit_outlined),
title: Text(context.l10n.channels_editChannel),
onTap: () async {
Navigator.pop(sheetContext);
Navigator.pop(context);
await Future.delayed(const Duration(milliseconds: 100));
if (parentContext.mounted) {
_showEditChannelDialog(parentContext, connector, channel);
}
},
),
ListTile(
leading: Icon(
isMuted
? Icons.notifications_outlined
: Icons.notifications_off_outlined,
),
title: Text(
isMuted
? context.l10n.channels_unmuteChannel
: context.l10n.channels_muteChannel,
),
onTap: () async {
Navigator.pop(sheetContext);
if (isMuted) {
await settingsService.unmuteChannel(channel.name);
} else {
await settingsService.muteChannel(channel.name);
if (context.mounted) {
_showEditChannelDialog(context, connector, channel);
}
},
),
@@ -543,15 +502,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
style: const TextStyle(color: Colors.red),
),
onTap: () async {
Navigator.pop(sheetContext);
Navigator.pop(context);
await Future.delayed(const Duration(milliseconds: 100));
if (parentContext.mounted) {
_confirmDeleteChannel(
context,
connector,
channelMessageStore,
channel,
);
if (context.mounted) {
_confirmDeleteChannel(context, connector, channel);
}
},
),
@@ -718,8 +672,6 @@ class _ChannelsScreenState extends State<ChannelsScreen>
bool isRegularHashtag = true;
Community? selectedCommunity;
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
showDialog(
context: context,
builder: (dialogContext) => StatefulBuilder(
@@ -771,9 +723,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
);
}
Widget? buildExpandedContent(
ChannelMessageStore channelMessageStore,
) {
Widget? buildExpandedContent() {
switch (selectedOption) {
case 0: // Create Private Channel
return Column(
@@ -798,7 +748,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
children: [
Expanded(
child: FilledButton(
onPressed: () async {
onPressed: () {
final name = nameController.text.trim();
if (name.isEmpty) {
ScaffoldMessenger.of(
@@ -820,14 +770,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
psk[i] = random.nextInt(256);
}
Navigator.pop(dialogContext);
await connector.setChannel(
nextIndex,
name,
psk,
);
await channelMessageStore.clearChannelMessages(
nextIndex,
);
connector.setChannel(nextIndex, name, psk);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
@@ -1346,8 +1289,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
subtitle:
dialogContext.l10n.channels_createPrivateChannelDesc,
),
if (selectedOption == 0)
buildExpandedContent(_channelMessageStore)!,
if (selectedOption == 0) buildExpandedContent()!,
const Divider(height: 1),
buildOptionTile(
optionIndex: 1,
@@ -1356,8 +1298,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
subtitle:
dialogContext.l10n.channels_joinPrivateChannelDesc,
),
if (selectedOption == 1)
buildExpandedContent(_channelMessageStore)!,
if (selectedOption == 1) buildExpandedContent()!,
if (!hasPublicChannel) ...[
const Divider(height: 1),
buildOptionTile(
@@ -1367,8 +1308,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
subtitle:
dialogContext.l10n.channels_joinPublicChannelDesc,
),
if (selectedOption == 2)
buildExpandedContent(_channelMessageStore)!,
if (selectedOption == 2) buildExpandedContent()!,
],
const Divider(height: 1),
buildOptionTile(
@@ -1378,8 +1318,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
subtitle:
dialogContext.l10n.channels_joinHashtagChannelDesc,
),
if (selectedOption == 3)
buildExpandedContent(_channelMessageStore)!,
if (selectedOption == 3) buildExpandedContent()!,
const Divider(height: 1),
buildOptionTile(
optionIndex: 4,
@@ -1387,8 +1326,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
title: dialogContext.l10n.community_scanQr,
subtitle: dialogContext.l10n.community_join,
),
if (selectedOption == 4)
buildExpandedContent(_channelMessageStore)!,
if (selectedOption == 4) buildExpandedContent()!,
const Divider(height: 1),
buildOptionTile(
optionIndex: 5,
@@ -1396,8 +1334,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
title: dialogContext.l10n.community_create,
subtitle: dialogContext.l10n.community_createDesc,
),
if (selectedOption == 5)
buildExpandedContent(_channelMessageStore)!,
if (selectedOption == 5) buildExpandedContent()!,
],
),
),
@@ -1478,7 +1415,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
child: Text(dialogContext.l10n.common_cancel),
),
FilledButton(
onPressed: () async {
onPressed: () {
final name = nameController.text.trim();
final pskHex = pskController.text.trim();
@@ -1495,25 +1432,13 @@ class _ChannelsScreenState extends State<ChannelsScreen>
}
Navigator.pop(dialogContext);
try {
await connector.setChannel(channel.index, name, psk);
await connector.setChannelSmazEnabled(
channel.index,
smazEnabled,
);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
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')),
);
}
connector.setChannel(channel.index, name, psk);
connector.setChannelSmazEnabled(channel.index, smazEnabled);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.channels_channelUpdated(name)),
),
);
},
child: Text(dialogContext.l10n.common_save),
),
@@ -1526,7 +1451,6 @@ class _ChannelsScreenState extends State<ChannelsScreen>
void _confirmDeleteChannel(
BuildContext context,
MeshCoreConnector connector,
ChannelMessageStore channelMessageStore,
Channel channel,
) {
showDialog(
@@ -1542,36 +1466,16 @@ class _ChannelsScreenState extends State<ChannelsScreen>
child: Text(dialogContext.l10n.common_cancel),
),
TextButton(
onPressed: () async {
onPressed: () {
Navigator.pop(dialogContext);
try {
await connector.deleteChannel(channel.index);
await channelMessageStore.clearChannelMessages(channel.index);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.channels_channelDeleted(channel.name),
),
connector.deleteChannel(channel.index);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
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),
),
),
);
// Preserve existing logging (if it was there)
debugPrint('Failed to delete channel: $e\n$st');
}
),
);
},
child: Text(
dialogContext.l10n.common_delete,
@@ -1772,7 +1676,6 @@ class _ChannelsScreenState extends State<ChannelsScreen>
}
final channelCount = communityChannels.length;
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
showDialog(
context: context,
File diff suppressed because it is too large Load Diff
-89
View File
@@ -1,89 +0,0 @@
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,
),
),
],
),
),
],
),
),
);
}
}
+1 -7
View File
@@ -6,7 +6,6 @@ import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../models/community.dart';
import '../storage/community_store.dart';
import '../widgets/adaptive_app_bar_title.dart';
import '../widgets/qr_scanner_widget.dart';
/// Screen for scanning community QR codes to join communities.
@@ -30,7 +29,7 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: AdaptiveAppBarTitle(context.l10n.community_scanQr),
title: Text(context.l10n.community_scanQr),
centerTitle: true,
),
body: _isProcessing
@@ -51,9 +50,6 @@ 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);
@@ -212,8 +208,6 @@ 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
+98 -217
View File
@@ -3,9 +3,6 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:meshcore_open/screens/path_trace_map.dart';
import 'package:meshcore_open/services/notification_service.dart';
import 'package:meshcore_open/utils/app_logger.dart';
import 'package:meshcore_open/widgets/app_bar.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
@@ -19,6 +16,7 @@ import '../utils/dialog_utils.dart';
import '../utils/disconnect_navigation_mixin.dart';
import '../utils/emoji_utils.dart';
import '../utils/route_transitions.dart';
import '../widgets/battery_indicator.dart';
import '../widgets/list_filter_widget.dart';
import '../widgets/empty_state.dart';
import '../widgets/quick_switch_bar.dart';
@@ -27,7 +25,6 @@ import '../widgets/room_login_dialog.dart';
import '../widgets/unread_badge.dart';
import 'channels_screen.dart';
import 'chat_screen.dart';
import 'discovery_screen.dart';
import 'map_screen.dart';
import 'repeater_hub_screen.dart';
import 'settings_screen.dart';
@@ -65,13 +62,6 @@ class _ContactsScreenState extends State<ContactsScreen>
super.initState();
_loadGroups();
_setupFrameListener();
_clearAdvertNotifications();
}
void _clearAdvertNotifications() {
final connector = context.read<MeshCoreConnector>();
final contactIds = connector.contacts.map((c) => c.publicKeyHex).toList();
NotificationService().clearAdvertNotifications(contactIds);
}
@override
@@ -100,90 +90,79 @@ class _ContactsScreenState extends State<ContactsScreen>
_frameSubscription = connector.receivedFrames.listen((frame) {
if (frame.isEmpty) return;
final frameBuffer = BufferReader(frame);
try {
final code = frameBuffer.readUInt8();
final code = frameBuffer.readUInt8();
if (code == respCodeExportContact) {
final advertPacket = frameBuffer.readRemainingBytes();
// Validate packet has expected minimum size (98+ bytes per protocol)
if (advertPacket.length < 98) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.contacts_invalidAdvertFormat),
),
);
}
_pendingOperations.remove(ContactOperationType.export);
return;
if (code == respCodeExportContact) {
final advertPacket = frameBuffer.readRemainingBytes();
// Validate packet has expected minimum size (98+ bytes per protocol)
if (advertPacket.length < 98) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.contacts_invalidAdvertFormat),
),
);
}
final hexString = pubKeyToHex(advertPacket);
Clipboard.setData(ClipboardData(text: "meshcore://$hexString"));
_pendingOperations.remove(ContactOperationType.export);
return;
}
final hexString = pubKeyToHex(advertPacket);
Clipboard.setData(ClipboardData(text: "meshcore://$hexString"));
}
if (code == respCodeOk) {
// Show a snackbar indicating success
if (!mounted) return;
if (_pendingOperations.contains(ContactOperationType.import)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_contactImported)),
);
}
if (code == respCodeOk) {
// Show a snackbar indicating success
if (!mounted) return;
if (_pendingOperations.contains(ContactOperationType.import)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_contactImported)),
);
}
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.contacts_zeroHopContactAdvertSent),
),
);
}
if (_pendingOperations.contains(ContactOperationType.export)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.contacts_contactAdvertCopied),
),
);
}
_pendingOperations.clear();
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.contacts_zeroHopContactAdvertSent),
),
);
}
if (code == respCodeErr) {
// Show a snackbar indicating failure
if (!mounted) return;
if (_pendingOperations.contains(ContactOperationType.import)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.contacts_contactImportFailed),
),
);
}
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.contacts_zeroHopContactAdvertFailed),
),
);
}
if (_pendingOperations.contains(ContactOperationType.export)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.contacts_contactAdvertCopyFailed),
),
);
}
_pendingOperations.clear();
if (_pendingOperations.contains(ContactOperationType.export)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)),
);
}
} catch (e) {
appLogger.error(
'Error processing received frame: $e',
tag: 'ContactsScreen',
);
_pendingOperations.clear();
}
if (code == respCodeErr) {
// Show a snackbar indicating failure
if (!mounted) return;
if (_pendingOperations.contains(ContactOperationType.import)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_contactImportFailed)),
);
}
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.contacts_zeroHopContactAdvertFailed),
),
);
}
if (_pendingOperations.contains(ContactOperationType.export)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.contacts_contactAdvertCopyFailed),
),
);
}
_pendingOperations.clear();
}
});
}
@@ -192,17 +171,14 @@ class _ContactsScreenState extends State<ContactsScreen>
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final exportContactFrame = buildExportContactFrame(pubKey);
_pendingOperations.add(ContactOperationType.export);
await connector.sendFrame(exportContactFrame, expectsGenericAck: true);
await connector.sendFrame(exportContactFrame);
}
Future<void> _contactZeroHop(Uint8List pubKey) async {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final exportContactZeroHopFrame = buildZeroHopContact(pubKey);
_pendingOperations.add(ContactOperationType.zeroHopShare);
await connector.sendFrame(
exportContactZeroHopFrame,
expectsGenericAck: true,
);
await connector.sendFrame(exportContactZeroHopFrame);
}
Future<void> _contactImport() async {
@@ -227,10 +203,9 @@ class _ContactsScreenState extends State<ContactsScreen>
}
final hexString = text.substring('meshcore://'.length);
try {
final bytes = hex2Uint8List(hexString);
final importContactFrame = buildImportContactFrame(bytes);
final importContactFrame = buildImportContactFrame(hexString);
_pendingOperations.add(ContactOperationType.import);
connector.importContact(importContactFrame);
await connector.sendFrame(importContactFrame);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -254,7 +229,9 @@ class _ContactsScreenState extends State<ContactsScreen>
canPop: allowBack,
child: Scaffold(
appBar: AppBar(
title: AppBarTitle(context.l10n.contacts_title),
leading: BatteryIndicator(connector: connector),
title: Text(context.l10n.contacts_title),
centerTitle: true,
automaticallyImplyLeading: false,
actions: [
PopupMenuButton(
@@ -328,21 +305,6 @@ class _ContactsScreenState extends State<ContactsScreen>
),
onTap: () => _disconnect(context, connector),
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.person_add_rounded),
const SizedBox(width: 8),
Text("Discovered Contacts"),
],
),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DiscoveryScreen(),
),
),
),
PopupMenuItem(
child: Row(
children: [
@@ -409,15 +371,8 @@ class _ContactsScreenState extends State<ContactsScreen>
Widget _buildContactsBody(BuildContext context, MeshCoreConnector connector) {
final contacts = connector.contacts;
final shouldShowStartupSpinner =
contacts.isEmpty &&
_groups.isEmpty &&
connector.isConnected &&
(connector.isLoadingContacts ||
connector.isLoadingChannels ||
connector.selfPublicKey == null);
if (shouldShowStartupSpinner) {
if (contacts.isEmpty && connector.isLoadingContacts && _groups.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
@@ -434,41 +389,6 @@ class _ContactsScreenState extends State<ContactsScreen>
? const <ContactGroup>[]
: _filterAndSortGroups(_groups, contacts);
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(
@@ -476,7 +396,7 @@ class _ContactsScreenState extends State<ContactsScreen>
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: hintText,
hintText: context.l10n.contacts_searchContacts,
prefixIcon: const Icon(Icons.search),
suffixIcon: Row(
mainAxisSize: MainAxisSize.min,
@@ -548,7 +468,6 @@ class _ContactsScreenState extends State<ContactsScreen>
contact: contact,
lastSeen: _resolveLastSeen(contact),
unreadCount: unreadCount,
isFavorite: contact.isFavorite,
onTap: () => _openChat(context, contact),
onLongPress: () =>
_showContactOptions(context, connector, contact),
@@ -585,8 +504,6 @@ class _ContactsScreenState extends State<ContactsScreen>
})
.where((group) {
if (_typeFilter == ContactTypeFilter.all) return true;
// Groups don't have a favorite flag, so hide them under favorites filter
if (_typeFilter == ContactTypeFilter.favorites) return false;
for (final key in group.memberKeys) {
final contact = contactsByKey[key];
if (contact != null && _matchesTypeFilter(contact)) return true;
@@ -661,8 +578,6 @@ class _ContactsScreenState extends State<ContactsScreen>
switch (_typeFilter) {
case ContactTypeFilter.all:
return true;
case ContactTypeFilter.favorites:
return contact.isFavorite;
case ContactTypeFilter.users:
return contact.type == advTypeChat;
case ContactTypeFilter.repeaters:
@@ -1053,7 +968,6 @@ class _ContactsScreenState extends State<ContactsScreen>
) {
final isRepeater = contact.type == advTypeRepeater;
final isRoom = contact.type == advTypeRoom;
final isFavorite = contact.isFavorite;
showModalBottomSheet(
context: context,
@@ -1146,7 +1060,6 @@ class _ContactsScreenState extends State<ContactsScreen>
contact.name,
),
path: contact.traceRouteBytes ?? Uint8List(0),
targetContact: contact,
),
),
);
@@ -1161,21 +1074,6 @@ class _ContactsScreenState extends State<ContactsScreen>
},
),
],
ListTile(
leading: Icon(
isFavorite ? Icons.star : Icons.star_border,
color: Colors.amber[700],
),
title: Text(
isFavorite
? context.l10n.listFilter_removeFromFavorites
: context.l10n.listFilter_addToFavorites,
),
onTap: () async {
Navigator.pop(sheetContext);
await connector.setContactFavorite(contact, !isFavorite);
},
),
ListTile(
leading: const Icon(Icons.copy),
title: Text(context.l10n.contacts_ShareContact),
@@ -1244,7 +1142,6 @@ class _ContactTile extends StatelessWidget {
final Contact contact;
final DateTime lastSeen;
final int unreadCount;
final bool isFavorite;
final VoidCallback onTap;
final VoidCallback onLongPress;
@@ -1252,7 +1149,6 @@ class _ContactTile extends StatelessWidget {
required this.contact,
required this.lastSeen,
required this.unreadCount,
required this.isFavorite,
required this.onTap,
required this.onLongPress,
});
@@ -1264,17 +1160,12 @@ class _ContactTile extends StatelessWidget {
backgroundColor: _getTypeColor(contact.type),
child: _buildContactAvatar(contact),
),
title: Text(contact.name, maxLines: 1, overflow: TextOverflow.ellipsis),
title: Text(contact.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(contact.pathLabel, maxLines: 1, overflow: TextOverflow.ellipsis),
Text(
contact.shortPubKeyHex,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12),
),
Text(contact.pathLabel),
Text(contact.shortPubKeyHex, style: TextStyle(fontSize: 12)),
],
),
// Clamp text scaling in trailing section to prevent overflow while
@@ -1285,36 +1176,26 @@ class _ContactTile extends StatelessWidget {
MediaQuery.textScalerOf(context).scale(1.0).clamp(1.0, 1.3),
),
),
child: SizedBox(
width: 120,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (unreadCount > 0) ...[
UnreadBadge(count: unreadCount),
const SizedBox(height: 4),
],
Text(
_formatLastSeen(context, lastSeen),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.right,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isFavorite)
Icon(Icons.star, size: 14, color: Colors.amber[700]),
if (isFavorite && contact.hasLocation)
const SizedBox(width: 2),
if (contact.hasLocation)
Icon(Icons.location_on, size: 14, color: Colors.grey[400]),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (unreadCount > 0) ...[
UnreadBadge(count: unreadCount),
const SizedBox(height: 4),
],
),
Text(
_formatLastSeen(context, lastSeen),
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]),
],
),
],
),
),
onTap: onTap,
-420
View File
@@ -1,420 +0,0 @@
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 '../widgets/app_bar.dart';
import '../widgets/list_filter_widget.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();
}
@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];
return 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,
),
trailing: Text(
_formatLastSeen(context, contact.lastSeen),
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
onTap: () {
connector.importDiscoveredContact(contact);
},
onLongPress: () =>
_showContactContextMenu(contact, connector),
);
},
),
),
],
),
);
}
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;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(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);
}
}
File diff suppressed because it is too large Load Diff
+1 -5
View File
@@ -7,7 +7,6 @@ import '../l10n/app_localizations.dart';
import '../l10n/l10n.dart';
import '../services/app_settings_service.dart';
import '../services/map_tile_cache_service.dart';
import '../widgets/adaptive_app_bar_title.dart';
class MapCacheScreen extends StatefulWidget {
const MapCacheScreen({super.key});
@@ -225,10 +224,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
: (_completedTiles / _estimatedTiles).clamp(0.0, 1.0).toDouble();
return Scaffold(
appBar: AppBar(
title: AdaptiveAppBarTitle(l10n.mapCache_title),
centerTitle: true,
),
appBar: AppBar(title: Text(l10n.mapCache_title), centerTitle: true),
body: Column(
children: [
Expanded(
+45 -695
View File
@@ -1,28 +1,22 @@
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:meshcore_open/screens/path_trace_map.dart';
import 'package:meshcore_open/widgets/app_bar.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../connector/meshcore_protocol.dart';
import '../models/app_settings.dart';
import '../models/channel.dart';
import '../models/contact.dart';
import '../services/app_settings_service.dart';
import '../services/path_history_service.dart';
import '../services/map_marker_service.dart';
import '../services/map_tile_cache_service.dart';
import '../utils/contact_search.dart';
import '../utils/route_transitions.dart';
import '../widgets/battery_indicator.dart';
import '../widgets/quick_switch_bar.dart';
import '../icons/los_icon.dart';
import 'channels_screen.dart';
import 'chat_screen.dart';
import 'contacts_screen.dart';
@@ -30,7 +24,6 @@ import '../widgets/repeater_login_dialog.dart';
import '../widgets/room_login_dialog.dart';
import 'repeater_hub_screen.dart';
import 'settings_screen.dart';
import 'line_of_sight_map_screen.dart';
class MapScreen extends StatefulWidget {
final LatLng? highlightPosition;
@@ -51,24 +44,14 @@ class MapScreen extends StatefulWidget {
}
class _MapScreenState extends State<MapScreen> {
// Zoom level at which node labels start to appear
static const double _labelZoomThreshold = 12.0;
final MapController _mapController = MapController();
final MapMarkerService _markerService = MapMarkerService();
final Set<String> _hiddenMarkerIds = {};
Set<String> _removedMarkerIds = {};
bool _isBuildingPathTrace = false;
bool _isSelectingPoi = false;
bool _hasInitializedMap = false;
bool _removedMarkersLoaded = false;
final List<int> _pathTrace = [];
final List<LatLng> _points = [];
final List<Polyline> _polylines = [];
bool _legendExpanded = false;
bool _showNodeLabels = true;
List<_GuessedLocation> _cachedGuessedLocations = [];
String _guessedLocationsCacheKey = '';
@override
void initState() {
@@ -93,15 +76,6 @@ class _MapScreenState extends State<MapScreen> {
});
}
bool _checkLocationPlausibility(double lat, double lon) {
const double epsilon = 1e-6;
return (lat.abs() > epsilon || lon.abs() > epsilon) &&
lat >= -90.0 &&
lat <= 90.0 &&
lon >= -180.0 &&
lon <= 180.0;
}
double _standardDeviation(List<double> values) {
if (values.length <= 1) {
return 0.0;
@@ -125,7 +99,7 @@ class _MapScreenState extends State<MapScreen> {
double _zoomFromStdDev(double latStdDev, double lonStdDev) {
final maxSpread = max(latStdDev, lonStdDev);
if (maxSpread <= 0) return 13.0;
// Approximate: each zoom level halves the visible area
// Approzimate: each zoom level halves the visible area
// ~0.01 degrees spread -> zoom 13, ~0.1 -> zoom 10, ~1.0 -> zoom 7
final zoom = 10.0 - log(maxSpread * 10 + 1) / ln10 * 3;
return zoom.clamp(4.0, 15.0);
@@ -133,19 +107,11 @@ class _MapScreenState extends State<MapScreen> {
@override
Widget build(BuildContext context) {
return Consumer3<MeshCoreConnector, AppSettingsService, PathHistoryService>(
builder: (context, connector, settingsService, pathHistory, child) {
return Consumer2<MeshCoreConnector, AppSettingsService>(
builder: (context, connector, settingsService, child) {
final tileCache = context.read<MapTileCacheService>();
final settings = settingsService.settings;
final allContacts = <Contact>[
...connector.contacts,
...connector.discoveredContacts.where((c) => !c.isActive),
];
final contacts = settings.mapShowDiscoveryContacts
? allContacts
: allContacts.where((c) => c.isActive).toList();
final contacts = connector.contacts;
final highlightPosition = widget.highlightPosition;
final sharedMarkers = settings.mapShowMarkers
? _collectSharedMarkers(connector)
@@ -178,64 +144,10 @@ class _MapScreenState extends State<MapScreen> {
: filteredByTime;
// Filter by location
final contactsWithLocation = filteredByKeyPrefix.where((c) {
if (!c.hasLocation) {
return false;
}
return _checkLocationPlausibility(c.latitude!, c.longitude!);
}).toList();
// All contacts with a known location used as anchors regardless of
// time/key-prefix filters so that repeaters are always available.
final allContactsWithLocation = allContacts
.where(
(c) =>
c.hasLocation &&
_checkLocationPlausibility(c.latitude!, c.longitude!),
)
final contactsWithLocation = filteredByKeyPrefix
.where((c) => c.hasLocation)
.toList();
// Compute guessed locations with caching
final maxRangeKm = _estimateLoRaRangeKm(connector);
final filteredKeys = filteredByKeyPrefix
.map((c) => '${c.publicKeyHex}:${c.path.join("-")}')
.join(',');
final anchorKeys = allContactsWithLocation
.map(
(c) =>
'${c.publicKeyHex}:${c.latitude}:${c.longitude}:${c.path.isNotEmpty ? c.path.last : ""}',
)
.join(',');
final cacheKey =
'$filteredKeys|$anchorKeys|${pathHistory.version}:${connector.currentSf}:${connector.currentBwHz}:${connector.currentTxPower}:${settings.mapShowGuessedLocations}';
if (cacheKey != _guessedLocationsCacheKey) {
_guessedLocationsCacheKey = cacheKey;
_cachedGuessedLocations = settings.mapShowGuessedLocations
? _computeGuessedLocations(
filteredByKeyPrefix,
allContactsWithLocation,
pathHistory,
maxRangeKm,
)
: [];
}
final guessedLocations = settings.mapShowGuessedLocations
? _cachedGuessedLocations
: <_GuessedLocation>[];
_polylines.clear();
_polylines.addAll(
_points.length > 1
? [
Polyline(
points: _points,
strokeWidth: 4,
color: Colors.blueAccent,
),
]
: <Polyline>[],
);
// Calculate center and zoom of all nodes, or default to (0, 0)
LatLng center = const LatLng(0, 0);
double initialZoom = 10.0;
@@ -316,7 +228,6 @@ class _MapScreenState extends State<MapScreen> {
// Re center map after removed markers have loaded
if (!_hasInitializedMap && _removedMarkersLoaded) {
_hasInitializedMap = true;
_showNodeLabels = initialZoom >= _labelZoomThreshold;
if (hasMapContent) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
@@ -332,57 +243,11 @@ class _MapScreenState extends State<MapScreen> {
canPop: allowBack,
child: Scaffold(
appBar: AppBar(
title: AppBarTitle(context.l10n.map_title),
leading: BatteryIndicator(connector: connector),
title: Text(context.l10n.map_title),
centerTitle: true,
automaticallyImplyLeading: false,
actions: [
if (!_isBuildingPathTrace)
IconButton(
icon: const Icon(Icons.radar),
onPressed: () => _startPath(),
tooltip: context.l10n.contacts_pathTrace,
),
if (!_isBuildingPathTrace)
IconButton(
icon: const LosIcon(),
onPressed: () {
final candidates = <LineOfSightEndpoint>[];
if (connector.selfLatitude != null &&
connector.selfLongitude != null) {
candidates.add(
LineOfSightEndpoint(
label: context.l10n.pathTrace_you,
point: LatLng(
connector.selfLatitude!,
connector.selfLongitude!,
),
color: Colors.teal,
icon: Icons.person_pin_circle,
),
);
}
for (final c in contactsWithLocation) {
candidates.add(
LineOfSightEndpoint(
label: c.name,
point: LatLng(c.latitude!, c.longitude!),
color: _getNodeColor(c.type),
icon: _getNodeIcon(c.type),
),
);
}
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LineOfSightMapScreen(
title: context.l10n.map_losScreenTitle,
candidates: candidates,
),
),
);
},
tooltip: context.l10n.map_lineOfSight,
),
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
@@ -410,6 +275,17 @@ class _MapScreenState extends State<MapScreen> {
),
),
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.refresh),
const SizedBox(width: 8),
Text(context.l10n.map_updateMyLocation),
],
),
onTap: () =>
connector.sparseLocationLogger?.updateMyLocation(),
),
],
icon: const Icon(Icons.more_vert),
),
@@ -461,14 +337,6 @@ class _MapScreenState extends State<MapScreen> {
position: latLng,
);
},
onPositionChanged: (camera, hasGesture) {
final shouldShow = camera.zoom >= _labelZoomThreshold;
if (shouldShow != _showNodeLabels && mounted) {
setState(() {
_showNodeLabels = shouldShow;
});
}
},
),
children: [
TileLayer(
@@ -478,8 +346,6 @@ class _MapScreenState extends State<MapScreen> {
MapTileCacheService.userAgentPackageName,
maxZoom: 19,
),
if (_polylines.isNotEmpty && _isBuildingPathTrace)
PolylineLayer(polylines: _polylines),
MarkerLayer(
markers: [
if (highlightPosition != null)
@@ -493,16 +359,7 @@ class _MapScreenState extends State<MapScreen> {
size: 34,
),
),
if (!_isBuildingPathTrace)
..._buildGuessedMarker(
guessedLocations,
showLabels: _showNodeLabels,
),
..._buildMarkers(
contactsWithLocation,
settings,
showLabels: _showNodeLabels,
),
..._buildMarkers(contactsWithLocation, settings),
...sharedMarkers.map(_buildSharedMarker),
if (connector.selfLatitude != null &&
connector.selfLongitude != null)
@@ -531,35 +388,21 @@ class _MapScreenState extends State<MapScreen> {
],
),
alignment: Alignment.center,
child: const Icon(
Icons.person_pin_circle,
color: Colors.white,
size: 20,
child: Text(
context.l10n.pathTrace_you,
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
),
if (_showNodeLabels &&
connector.selfLatitude != null &&
connector.selfLongitude != null)
_buildNodeLabelMarker(
point: LatLng(
connector.selfLatitude!,
connector.selfLongitude!,
),
label: context.l10n.pathTrace_you,
),
],
),
],
),
if (!_isBuildingPathTrace)
_buildLegend(
contactsWithLocation,
settings,
sharedMarkers.length,
guessedLocations.length,
),
if (_isBuildingPathTrace) _buildPathTraceOverlay(),
_buildLegend(contactsWithLocation.length, sharedMarkers.length),
],
),
bottomNavigationBar: SafeArea(
@@ -581,256 +424,20 @@ class _MapScreenState extends State<MapScreen> {
);
}
List<_GuessedLocation> _computeGuessedLocations(
List<Contact> allContacts,
List<Contact> withLocation,
PathHistoryService pathHistory,
double? maxRangeKm,
) {
// Index known-location repeaters by their 1-byte hash.
// null value = two repeaters share the same hash byte (ambiguous collision).
final repeaterByHash = <int, Contact?>{};
for (final c in withLocation) {
if (c.type == advTypeRepeater) {
if (repeaterByHash.containsKey(c.publicKey[0])) {
repeaterByHash[c.publicKey[0]] =
null; // collision: can't disambiguate
} else {
repeaterByHash[c.publicKey[0]] = c;
}
}
}
final result = <_GuessedLocation>[];
for (final contact in allContacts) {
if (contact.hasLocation) continue;
final anchorSet = <LatLng>{};
// Collect the contact-side (last-hop) repeater from every known path.
// path = [device-side hop, ..., contact-side hop]
// Only path.last is actually within radio range of the contact using
// earlier bytes would anchor against our own side of the network.
final pathSets = <List<int>>[
contact.path.toList(),
...pathHistory
.getRecentPaths(contact.publicKeyHex)
.map((r) => r.pathBytes),
];
final lastHopBytes = <int>{};
for (final pathBytes in pathSets) {
if (pathBytes.isEmpty) continue;
final lastHop = pathBytes.last;
lastHopBytes.add(lastHop);
final r = repeaterByHash[lastHop];
if (r != null) anchorSet.add(LatLng(r.latitude!, r.longitude!));
}
// Fallback: for any last-hop byte with no GPS repeater, average the
// positions of contacts with known GPS that share the same last hop.
// Those contacts are all adjacent to the same unknown repeater, so their
// centroid is a reasonable proxy for its location.
for (final byte in lastHopBytes) {
if (repeaterByHash.containsKey(byte)) continue;
for (final c in withLocation) {
if (c.path.isNotEmpty && c.path.last == byte) {
anchorSet.add(LatLng(c.latitude!, c.longitude!));
}
}
}
// Filter anchors that are geometrically inconsistent with radio range.
// Two anchors more than 2 * maxRange apart cannot both be in direct radio
// range of the same node, so isolated outliers are removed.
final anchors = maxRangeKm != null && anchorSet.length > 1
? _filterConsistentAnchors(anchorSet.toList(), maxRangeKm)
: anchorSet.toList();
if (anchors.isEmpty) continue;
final LatLng position;
if (anchors.length == 1) {
// Offset single-anchor guesses so they don't overlap the repeater marker.
// Use the contact's public key byte as a deterministic angle seed.
const offsetDeg = 0.003; // ~330 m at the equator
final angle = (contact.publicKey[1] / 255.0) * 2 * pi;
position = LatLng(
anchors[0].latitude + offsetDeg * cos(angle),
anchors[0].longitude + offsetDeg * sin(angle),
);
if (!_checkLocationPlausibility(
position.latitude,
position.longitude,
)) {
continue; // discard implausible guesses near (0, 0)
}
} else {
double lat = 0, lon = 0;
for (final a in anchors) {
lat += a.latitude;
lon += a.longitude;
}
position = LatLng(lat / anchors.length, lon / anchors.length);
if (!_checkLocationPlausibility(
position.latitude,
position.longitude,
)) {
continue; // discard implausible guesses near (0, 0
}
}
result.add(
_GuessedLocation(
contact: contact,
position: position,
highConfidence: anchors.length >= 2,
),
);
}
return result;
}
/// Estimates the free-space maximum LoRa range in km from the connected
/// device's current radio parameters. Returns null if parameters are unknown.
double? _estimateLoRaRangeKm(MeshCoreConnector connector) {
final freqHz = connector.currentFreqHz;
final bwHz = connector.currentBwHz;
final sf = connector.currentSf;
final txPower = connector.currentTxPower;
if (freqHz == null || bwHz == null || sf == null || txPower == null) {
return null;
}
// LoRa receiver sensitivity = thermal noise + NF + required demod SNR
const noiseFigureDb = 6.0;
final thermalNoiseDbm = -174.0 + 10 * log(bwHz.toDouble()) / ln10;
final sensitivityDbm =
thermalNoiseDbm + noiseFigureDb + _sfToRequiredSnrDb(sf);
// FSPL at max range equals link budget:
// FSPL = 20*log10(d_m) + 20*log10(f_hz) - 147.55
final linkBudgetDb = txPower.toDouble() - sensitivityDbm;
final exponent =
(linkBudgetDb + 147.55 - 20 * log(freqHz.toDouble()) / ln10) / 20;
return pow(10, exponent) / 1000;
}
double _sfToRequiredSnrDb(int sf) {
switch (sf) {
case 5:
return -2.5;
case 6:
return -5.0;
case 7:
return -7.5;
case 8:
return -10.0;
case 9:
return -12.5;
case 10:
return -15.0;
case 11:
return -17.5;
case 12:
return -20.0;
default:
return -10.0;
}
}
/// Removes anchors that have no neighbour within 2 * maxRangeKm.
/// A node cannot be simultaneously in radio range of two points farther apart
/// than twice the expected maximum range.
List<LatLng> _filterConsistentAnchors(
List<LatLng> anchors,
double maxRangeKm,
) {
const distance = Distance();
final maxDistM = maxRangeKm * 2000;
return anchors
.where((a) => anchors.any((b) => b != a && distance(a, b) <= maxDistM))
.toList();
}
List<Marker> _buildGuessedMarker(
List<_GuessedLocation> guessed, {
required bool showLabels,
}) {
final markers = <Marker>[];
for (final guess in guessed) {
final color = _getNodeColor(guess.contact.type);
final marker = Marker(
point: guess.position,
width: 35,
height: 35,
child: GestureDetector(
onTap: () => _showNodeInfo(
context,
guess.contact,
guessedPosition: guess.position,
),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: color.withValues(
alpha: guess.highConfidence ? 0.55 : 0.30,
),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: const Icon(
Icons.not_listed_location,
color: Colors.white,
size: 20,
),
),
),
);
markers.add(marker);
if (showLabels) {
markers.add(
_buildNodeLabelMarker(
point: guess.position,
label: guess.contact.name,
),
);
}
}
return markers;
}
List<Marker> _buildMarkers(
List<Contact> contacts,
settings, {
required bool showLabels,
}) {
List<Marker> _buildMarkers(List<Contact> contacts, settings) {
final markers = <Marker>[];
for (final contact in contacts) {
if (!contact.hasLocation) continue;
// Apply node type filters
if (contact.type == advTypeRepeater &&
(!settings.mapShowRepeaters && !_isBuildingPathTrace)) {
continue;
}
if (contact.type == advTypeChat &&
!(settings.mapShowChatNodes && !_isBuildingPathTrace)) {
if (contact.type == advTypeRepeater && !settings.mapShowRepeaters) {
continue;
}
if (contact.type == advTypeChat && !settings.mapShowChatNodes) continue;
if (contact.type != advTypeChat &&
contact.type != advTypeRepeater &&
(!settings.mapShowOtherNodes && !_isBuildingPathTrace)) {
!settings.mapShowOtherNodes) {
continue;
}
@@ -839,11 +446,7 @@ class _MapScreenState extends State<MapScreen> {
width: 35,
height: 35,
child: GestureDetector(
onLongPress: () =>
_isBuildingPathTrace ? _showNodeInfo(context, contact) : null,
onTap: () => _isBuildingPathTrace
? _addToPath(context, contact)
: _showNodeInfo(context, contact),
onTap: () => _showNodeInfo(context, contact),
child: Column(
children: [
Container(
@@ -872,54 +475,11 @@ class _MapScreenState extends State<MapScreen> {
);
markers.add(marker);
if (showLabels) {
markers.add(
_buildNodeLabelMarker(
point: LatLng(contact.latitude!, contact.longitude!),
label: contact.name,
),
);
}
}
return markers;
}
Marker _buildNodeLabelMarker({required LatLng point, required String label}) {
return Marker(
point: point,
width: 120,
height: 24,
alignment: Alignment.topCenter,
child: IgnorePointer(
child: Transform.translate(
offset: const Offset(0, -20),
child: FittedBox(
fit: BoxFit.contain,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
),
),
),
),
);
}
Color _getNodeColor(int type) {
switch (type) {
case advTypeChat:
@@ -950,27 +510,7 @@ class _MapScreenState extends State<MapScreen> {
}
}
Widget _buildLegend(
List<Contact> contactsWithLocation,
settings,
int markerCount,
int guessedCount,
) {
int nodeCount = 0;
for (final contact in contactsWithLocation) {
// Apply node type filters
if (contact.type == advTypeRepeater && !settings.mapShowRepeaters) {
continue;
}
if (contact.type == advTypeChat && !settings.mapShowChatNodes) continue;
if (contact.type != advTypeChat &&
contact.type != advTypeRepeater &&
!settings.mapShowOtherNodes) {
continue;
}
nodeCount++;
}
Widget _buildLegend(int nodeCount, int markerCount) {
return Positioned(
top: 16,
right: 16,
@@ -994,12 +534,7 @@ class _MapScreenState extends State<MapScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.map_nodesCount(
nodeCount +
(settings.mapShowGuessedLocations
? guessedCount
: 0),
),
context.l10n.map_nodesCount(nodeCount),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
@@ -1067,12 +602,6 @@ class _MapScreenState extends State<MapScreen> {
context.l10n.map_pinPublic,
Colors.orange,
),
if (settings.mapShowGuessedLocations && guessedCount > 0)
_buildLegendItem(
Icons.not_listed_location,
context.l10n.map_guessedLocation,
Colors.grey,
),
],
),
),
@@ -1089,7 +618,7 @@ class _MapScreenState extends State<MapScreen> {
Widget _buildLegendItem(IconData icon, String label, Color color) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 1.0),
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
@@ -1261,12 +790,7 @@ class _MapScreenState extends State<MapScreen> {
);
}
void _showNodeInfo(
BuildContext context,
Contact contact, {
LatLng? guessedPosition,
}) {
final connector = context.read<MeshCoreConnector>();
void _showNodeInfo(BuildContext context, Contact contact) {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
@@ -1277,7 +801,7 @@ class _MapScreenState extends State<MapScreen> {
color: _getNodeColor(contact.type),
),
const SizedBox(width: 8),
Expanded(child: SelectableText(contact.name)),
Expanded(child: Text(contact.name)),
],
),
content: Column(
@@ -1286,16 +810,10 @@ class _MapScreenState extends State<MapScreen> {
children: [
_buildInfoRow('Type', contact.typeLabel),
_buildInfoRow('Path', contact.pathLabel),
if (contact.hasLocation)
_buildInfoRow(
'Location',
'${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}',
)
else if (guessedPosition != null)
_buildInfoRow(
'Est. Location',
'~${guessedPosition.latitude.toStringAsFixed(6)}, ${guessedPosition.longitude.toStringAsFixed(6)}',
),
_buildInfoRow(
'Location',
'${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}',
),
_buildInfoRow(
context.l10n.map_lastSeen,
_formatLastSeen(contact.lastSeen),
@@ -1312,9 +830,6 @@ class _MapScreenState extends State<MapScreen> {
advTypeChat) // Only show chat button for chat nodes
TextButton(
onPressed: () {
if (!contact.isActive) {
connector.importDiscoveredContact(contact);
}
Navigator.pop(dialogContext);
Navigator.push(
context,
@@ -1328,9 +843,6 @@ class _MapScreenState extends State<MapScreen> {
if (contact.type == advTypeRepeater)
TextButton(
onPressed: () {
if (!contact.isActive) {
connector.importDiscoveredContact(contact);
}
Navigator.pop(dialogContext);
_showRepeaterLogin(context, contact);
},
@@ -1339,9 +851,6 @@ class _MapScreenState extends State<MapScreen> {
if (contact.type == advTypeRoom)
TextButton(
onPressed: () {
if (!contact.isActive) {
connector.importDiscoveredContact(contact);
}
Navigator.pop(dialogContext);
_showRoomLogin(context, contact);
},
@@ -1463,7 +972,7 @@ class _MapScreenState extends State<MapScreen> {
),
),
const SizedBox(height: 2),
SelectableText(value, style: const TextStyle(fontSize: 14)),
Text(value, style: const TextStyle(fontSize: 14)),
],
),
);
@@ -1509,23 +1018,6 @@ class _MapScreenState extends State<MapScreen> {
);
},
),
ListTile(
leading: const Icon(Icons.my_location),
title: Text(context.l10n.map_setAsMyLocation),
onTap: () async {
final messenger = ScaffoldMessenger.of(context);
final successMsg = context.l10n.settings_locationUpdated;
Navigator.pop(sheetContext);
if (!connector.isConnected) return;
await connector.setNodeLocation(
lat: position.latitude,
lon: position.longitude,
);
await connector.refreshDeviceInfo();
if (!mounted) return;
messenger.showSnackBar(SnackBar(content: Text(successMsg)));
},
),
ListTile(
leading: const Icon(Icons.close),
title: Text(context.l10n.common_cancel),
@@ -1647,8 +1139,7 @@ class _MapScreenState extends State<MapScreen> {
padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
child: TextField(
decoration: InputDecoration(
hintText:
context.l10n.contacts_searchContactsNoNumber,
hintText: context.l10n.contacts_searchContacts,
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
@@ -1827,22 +1318,6 @@ class _MapScreenState extends State<MapScreen> {
},
contentPadding: EdgeInsets.zero,
),
CheckboxListTile(
title: Text(context.l10n.map_showGuessedLocations),
value: settings.mapShowGuessedLocations,
onChanged: (value) {
service.setMapShowGuessedLocations(value ?? true);
},
contentPadding: EdgeInsets.zero,
),
CheckboxListTile(
title: Text(context.l10n.map_showDiscoveryContacts),
value: settings.mapShowDiscoveryContacts,
onChanged: (value) {
service.setMapShowDiscoveryContacts(value ?? true);
},
contentPadding: EdgeInsets.zero,
),
const SizedBox(height: 16),
Text(
context.l10n.map_keyPrefix,
@@ -1991,131 +1466,6 @@ class _MapScreenState extends State<MapScreen> {
return context.l10n.time_allTime;
}
}
void _addToPath(BuildContext context, Contact contact) {
setState(() {
_pathTrace.add(
contact.publicKey[0],
); // Add first 16 bytes of public key to path trace
_points.add(LatLng(contact.latitude!, contact.longitude!));
});
}
void _startPath() {
setState(() {
_isBuildingPathTrace = true;
_pathTrace.clear();
_points.clear();
_polylines.clear();
});
}
void _removePath() {
setState(() {
_pathTrace.removeLast(); // Remove last node from path trace
_points.removeLast(); // Remove last point from points list
_polylines.clear(); // Clear polylines
});
}
Widget _buildPathTraceOverlay() {
final l10n = context.l10n;
final isImperial =
context.read<AppSettingsService>().settings.unitSystem ==
UnitSystem.imperial;
return Positioned(
top: 16,
left: 16,
right: 16,
child: Card(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.contacts_pathTrace,
style: TextStyle(fontWeight: FontWeight.bold),
),
if (_pathTrace.isEmpty) const SizedBox(height: 8),
if (_pathTrace.isEmpty)
Text(l10n.map_tapToAdd, style: TextStyle(fontSize: 12)),
const SizedBox(height: 6),
if (_pathTrace.isNotEmpty)
Text(
"${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points), isImperial: isImperial)}",
style: TextStyle(fontSize: 12, color: Colors.grey[700]),
),
SelectableText(
_pathTrace
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join(','),
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 6),
Wrap(
alignment: WrapAlignment.center,
spacing: 8,
runSpacing: 8,
children: [
if (_pathTrace.isNotEmpty)
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PathTraceMapScreen(
title: l10n.contacts_pathTrace,
path: Uint8List.fromList(_pathTrace),
),
),
);
setState(() {
_isBuildingPathTrace = false;
});
},
child: Text(l10n.map_runTrace),
),
if (_pathTrace.isNotEmpty)
ElevatedButton(
onPressed: _removePath,
child: Text(l10n.map_removeLast),
),
if (_pathTrace.isEmpty)
ElevatedButton(
onPressed: () {
setState(() {
_isBuildingPathTrace = false;
_pathTrace.clear();
_points.clear();
_polylines.clear();
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.map_pathTraceCancelled)),
);
},
child: Text(l10n.common_cancel),
),
],
),
],
),
),
),
);
}
}
class _GuessedLocation {
final Contact contact;
final LatLng position;
final bool highConfidence;
_GuessedLocation({
required this.contact,
required this.position,
required this.highConfidence,
});
}
class _MarkerPayload {
@@ -1,7 +1,6 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:meshcore_open/utils/app_logger.dart';
import 'package:provider/provider.dart';
import '../l10n/l10n.dart';
import '../models/contact.dart';
@@ -12,28 +11,28 @@ import '../services/repeater_command_service.dart';
import '../widgets/path_management_dialog.dart';
import '../widgets/snr_indicator.dart';
class NeighborsScreen extends StatefulWidget {
class NeighboursScreen extends StatefulWidget {
final Contact repeater;
final String password;
const NeighborsScreen({
const NeighboursScreen({
super.key,
required this.repeater,
required this.password,
});
@override
State<NeighborsScreen> createState() => _NeighborsScreenState();
State<NeighboursScreen> createState() => _NeighboursScreenState();
}
class _NeighborsScreenState extends State<NeighborsScreen> {
static const int _reqNeighborsKeyLen = 4;
class _NeighboursScreenState extends State<NeighboursScreen> {
static const int _reqNeighboursKeyLen = 4;
static const int _statusPayloadOffset = 8;
static const int _statusStatsSize = 52;
static const int _statusResponseBytes =
_statusPayloadOffset + _statusStatsSize;
Uint8List _tagData = Uint8List(4);
int _neighborCount = 0;
int _neighbourCount = 0;
bool _isLoading = false;
bool _isLoaded = false;
@@ -42,7 +41,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
StreamSubscription<Uint8List>? _frameSubscription;
RepeaterCommandService? _commandService;
PathSelection? _pendingStatusSelection;
List<Map<String, dynamic>>? _parsedNeighbors;
List<Map<String, dynamic>>? _parsedNeighbours;
@override
void initState() {
@@ -50,7 +49,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
_commandService = RepeaterCommandService(connector);
_setupMessageListener();
_loadNeighbors();
_loadNeighbours();
_hasData = false;
}
@@ -63,12 +62,13 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
if (frame[0] == respCodeSent) {
_tagData = frame.sublist(2, 6);
//_timeEstment = frame.buffer.asByteData().getUint32(6, Endian.little);
}
// Check if it's a binary response
if (frame[0] == pushCodeBinaryResponse &&
listEquals(frame.sublist(2, 6), _tagData)) {
_handleNeighborsResponse(connector, frame.sublist(6));
_handleNeighboursResponse(connector, frame.sublist(6));
}
});
}
@@ -91,79 +91,65 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
return '${h}h ${m2}m';
}
static List<Map<String, dynamic>> parseNeighborsData(
static List<Map<String, dynamic>> parseNeighboursData(
BufferReader buffer,
int resultsCount,
) {
final Map<int, Map<String, dynamic>> neighbors = {};
try {
for (var i = 0; i < resultsCount; i++) {
final neighborData = neighbors.putIfAbsent(
i,
() => {
'contact': null,
'publicKey': <Uint8List>{},
'lastHeard': <int>{},
'snr': <double>{},
},
);
neighborData['publicKey'] = buffer.readBytes(_reqNeighborsKeyLen);
neighborData['lastHeard'] = buffer.readUInt32LE();
neighborData['snr'] = buffer.readInt8() / 4.0;
}
return neighbors.values.toList();
} catch (e) {
appLogger.error(
'Error parsing neighbors data: $e',
tag: 'NeighborsScreen',
final Map<int, Map<String, dynamic>> neighbours = {};
for (var i = 0; i < resultsCount; i++) {
final neighbourData = neighbours.putIfAbsent(
i,
() => {
'contact': null,
'publicKey': <Uint8List>{},
'lastHeard': <int>{},
'snr': <double>{},
},
);
return [];
neighbourData['publicKey'] = buffer.readBytes(_reqNeighboursKeyLen);
neighbourData['lastHeard'] = buffer.readUInt32LE();
neighbourData['snr'] = buffer.readInt8() / 4.0;
}
return neighbours.values.toList();
}
void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) {
void _handleNeighboursResponse(MeshCoreConnector connector, Uint8List frame) {
final buffer = BufferReader(frame);
final contacts = <Contact>[
...connector.contacts,
...connector.discoveredContacts,
];
try {
final neighborCount = buffer.readUInt16LE();
final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE());
contacts.where((c) => c.type == advTypeRepeater).forEach((repeater) {
for (var neighborData in parsedNeighbors) {
final publicKey = neighborData['publicKey'];
if (listEquals(
repeater.publicKey.sublist(0, _reqNeighborsKeyLen),
publicKey,
)) {
neighborData['contact'] = repeater;
}
final neighbourCount = buffer.readUInt16LE();
final parsedNeighbours = parseNeighboursData(buffer, buffer.readUInt16LE());
connector.contacts.where((c) => c.type == advTypeRepeater).forEach((
repeater,
) {
for (var neighbourData in parsedNeighbours) {
final publicKey = neighbourData['publicKey'];
if (listEquals(
repeater.publicKey.sublist(0, _reqNeighboursKeyLen),
publicKey,
)) {
neighbourData['contact'] = repeater;
}
});
}
});
setState(() {
_parsedNeighbors = parsedNeighbors;
_neighborCount = neighborCount;
});
setState(() {
_parsedNeighbours = parsedNeighbours;
_neighbourCount = neighbourCount;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.neighbors_receivedData),
backgroundColor: Colors.green,
),
);
_statusTimeout?.cancel();
if (!mounted) return;
setState(() {
_isLoading = false;
_isLoaded = true;
_hasData = true;
});
} catch (e) {
appLogger.error('Error handling neighbors response: $e');
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.neighbors_receivedData),
backgroundColor: Colors.green,
),
);
_statusTimeout?.cancel();
if (!mounted) return;
setState(() {
_isLoading = false;
_isLoaded = true;
_hasData = true;
});
}
Contact _resolveRepeater(MeshCoreConnector connector) {
@@ -173,7 +159,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
);
}
Future<void> _loadNeighbors() async {
Future<void> _loadNeighbours() async {
if (_commandService == null) return;
setState(() {
@@ -186,17 +172,17 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
final selection = await connector.preparePathForContactSend(repeater);
_pendingStatusSelection = selection;
//[version][number of requested neighbors][offset_16bit][order by][len of public key]
//[version][number of requested neighbours][offset_16bit][order by][len of public key]
final frame = buildSendBinaryReq(
repeater.publicKey,
payload: Uint8List.fromList([
reqTypeGetNeighbors,
reqTypeGetNeighbours,
0x00,
0x0F,
0x00,
0x00,
0x00,
_reqNeighborsKeyLen,
_reqNeighboursKeyLen,
]),
);
await connector.sendFrame(frame);
@@ -272,7 +258,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.neighbors_repeatersNeighbors,
l10n.neighbors_repeatersNeighbours,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Text(
@@ -359,7 +345,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.refresh),
onPressed: _isLoading ? null : _loadNeighbors,
onPressed: _isLoading ? null : _loadNeighbours,
tooltip: l10n.repeater_refresh,
),
],
@@ -367,13 +353,13 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
body: SafeArea(
top: false,
child: RefreshIndicator(
onRefresh: _loadNeighbors,
onRefresh: _loadNeighbours,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
if (!_isLoaded &&
!_hasData &&
(_parsedNeighbors == null || _parsedNeighbors!.isEmpty))
(_parsedNeighbours == null || _parsedNeighbours!.isEmpty))
Center(
child: Text(
l10n.neighbors_noData,
@@ -382,9 +368,10 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
),
if (_isLoaded ||
_hasData &&
!(_parsedNeighbors == null || _parsedNeighbors!.isEmpty))
_buildNeighborsInfoCard(
"${l10n.repeater_neighbors} - $_neighborCount",
!(_parsedNeighbours == null ||
_parsedNeighbours!.isEmpty))
_buildNeighboursInfoCard(
"${l10n.repeater_neighbours} - $_neighbourCount",
),
],
),
@@ -393,7 +380,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
);
}
Widget _buildNeighborsInfoCard(String title) {
Widget _buildNeighboursInfoCard(String title) {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
return Card(
child: Padding(
@@ -418,7 +405,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
],
),
const Divider(),
for (final entry in _parsedNeighbors!.asMap().entries)
for (final entry in _parsedNeighbours!.asMap().entries)
_buildInfoRow(
entry.value['contact'] != null
? entry.value['contact'].name
@@ -443,7 +430,6 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
double snr,
int spreadingFactor,
) {
final snrUi = snrUiFromSNR(snr, spreadingFactor);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
@@ -457,15 +443,9 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(value),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(snrUi.icon, color: snrUi.color, size: 18.0),
Text(
snrUi.text,
style: TextStyle(fontSize: 10, color: snrUi.color),
),
],
trailing: SNRIcon(
snr: snr,
snrLevels: getSNRfromSF(spreadingFactor),
),
),
),
+201 -494
View File
@@ -8,37 +8,14 @@ import 'package:latlong2/latlong.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart';
import 'package:meshcore_open/l10n/l10n.dart';
import 'package:meshcore_open/models/app_settings.dart';
import 'package:meshcore_open/models/contact.dart';
import 'package:meshcore_open/services/app_settings_service.dart';
import 'package:meshcore_open/services/map_tile_cache_service.dart';
import 'package:meshcore_open/utils/app_logger.dart';
import 'package:meshcore_open/widgets/snr_indicator.dart';
import 'package:provider/provider.dart';
double getPathDistanceMeters(List<LatLng> points) {
if (points.length <= 1) return 0.0;
double distanceMeters = 0.0;
final distanceCalculator = Distance();
for (int i = 0; i < points.length - 1; i++) {
distanceMeters += distanceCalculator(points[i], points[i + 1]);
}
return distanceMeters;
}
String formatDistance(double distanceMeters, {required bool isImperial}) {
if (isImperial) {
return '(${(distanceMeters / 1609.34).toStringAsFixed(2)} mi)';
}
return '(${(distanceMeters / 1000).toStringAsFixed(2)} km)';
}
class PathTraceData {
final Uint8List pathData;
final List<double> snrData;
final Uint8List snrData;
final Map<int, Contact> pathContacts;
PathTraceData({
@@ -51,19 +28,15 @@ class PathTraceData {
class PathTraceMapScreen extends StatefulWidget {
final String title;
final Uint8List path;
final int? repeaterId;
final bool flipPathRound;
final bool reversePathRound;
final Contact? targetContact;
const PathTraceMapScreen({
super.key,
required this.title,
required this.path,
this.repeaterId,
this.flipPathRound = false,
this.reversePathRound = false,
this.targetContact,
});
@override
@@ -71,28 +44,21 @@ class PathTraceMapScreen extends StatefulWidget {
}
class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
static const double _labelZoomThreshold = 8.5;
StreamSubscription<Uint8List>? _frameSubscription;
Timer? _timeoutTimer;
bool _isLoading = false;
bool _failed2Loaded = false;
bool _hasData = false;
bool _noLocationErr = 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);
double _initialZoom = 2.0;
LatLngBounds? _bounds;
ValueKey<String> _mapKey = const ValueKey('initial');
double _pathDistanceMeters = 0.0;
bool _showNodeLabels = true;
double _pathDistance = 0.0;
String _formatPathPrefixes(Uint8List pathBytes) {
return pathBytes
@@ -114,47 +80,36 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
super.dispose();
}
Uint8List buildPath(Uint8List pathBytes) {
Uint8List traceBytes;
if (pathBytes.isEmpty) {
traceBytes = Uint8List(1);
traceBytes[0] = widget.targetContact?.publicKey[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];
}
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];
}
}
return traceBytes;
}
double getPathDistance() {
double totalDistance = 0.0;
final distanceCalculator = Distance();
for (int i = 0; i < _points.length - 1; i++) {
totalDistance += distanceCalculator(_points[i], _points[i + 1]);
}
return totalDistance;
}
Future<void> _doPathTrace() async {
if (mounted) {
setState(() {
_isLoading = true;
_failed2Loaded = false;
_noLocationErr = false;
});
}
@@ -165,16 +120,11 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
: widget.path;
if (widget.flipPathRound) {
path = buildPath(pathTmp);
path = addReturnpath(pathTmp);
} else {
path = pathTmp;
}
appLogger.info(
'Initiating path trace with path: ${_formatPathPrefixes(path)}',
tag: 'PathTraceMapScreen',
);
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final frame = buildTraceReq(
DateTime.now().millisecondsSinceEpoch ~/ 1000,
@@ -192,57 +142,34 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
_frameSubscription = connector.receivedFrames.listen((frame) {
if (frame.isEmpty) return;
final frameBuffer = BufferReader(frame);
try {
final code = frameBuffer.readUInt8();
final code = frameBuffer.readUInt8();
if (code == respCodeSent) {
frameBuffer.skipBytes(1); //reserved
tagData = frameBuffer.readBytes(4);
final timeoutMilliseconds = frameBuffer.readUInt32LE();
if (code == respCodeSent) {
frameBuffer.skipBytes(1); //reserved
tagData = frameBuffer.readBytes(4);
final timeoutSeconds = frameBuffer.readUInt32LE();
// Start timeout timer for trace response
_timeoutTimer?.cancel();
_timeoutTimer = Timer(
Duration(milliseconds: timeoutMilliseconds),
() {
if (!mounted) return;
setState(() {
_isLoading = false;
_failed2Loaded = true;
});
},
);
}
if (code == respCodeErr) {
_timeoutTimer?.cancel();
// Start timeout timer for trace response
_timeoutTimer?.cancel();
_timeoutTimer = Timer(Duration(milliseconds: timeoutSeconds), () {
if (!mounted) return;
setState(() {
_isLoading = false;
_failed2Loaded = true;
});
}
});
}
// Check if it's a binary response
if (frame.length > 8 &&
code == pushCodeTraceData &&
listEquals(frame.sublist(4, 8), tagData)) {
_timeoutTimer?.cancel();
if (!mounted) return;
frameBuffer.skipBytes(3); //reserved + path length + flag
if (listEquals(frameBuffer.readBytes(4), tagData)) {
_handleTraceResponse(frame);
}
}
} catch (e) {
// Check if it's a binary response
if (frame.length > 8 &&
code == pushCodeTraceData &&
listEquals(frame.sublist(4, 8), tagData)) {
_timeoutTimer?.cancel();
if (!mounted) return;
setState(() {
_isLoading = false;
_failed2Loaded = true;
});
// Handle any parsing errors gracefully
appLogger.error('Error parsing frame: $e', tag: 'PathTraceMapScreen');
frameBuffer.skipBytes(3); //reserved + path length + flag
if (listEquals(frameBuffer.readBytes(4), tagData)) {
_handleTraceResponse(frame);
}
}
});
}
@@ -251,158 +178,71 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final buffer = BufferReader(frame);
try {
buffer.skipBytes(2); // Skip push code and reserved byte
int pathLength = buffer.readUInt8();
buffer.skipBytes(5); // Skip Flag byte and tag data
buffer.skipBytes(4); // Skip auth code
Uint8List pathData = buffer.readBytes(pathLength);
List<double> snrData = buffer
.readRemainingBytes()
.map((snr) => snr.toSigned(8).toDouble() / 4)
.toList();
buffer.skipBytes(2); // Skip push code and reserved byte
int pathLength = buffer.readUInt8();
buffer.skipBytes(5); // Skip Flag byte and tag data
buffer.skipBytes(4); // Skip auth code
Uint8List pathData = buffer.readBytes(pathLength);
Uint8List snrData = buffer.readRemainingBytes();
Map<int, Contact> pathContacts = {};
final contacts = <Contact>[
...connector.contacts,
...connector.discoveredContacts,
];
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;
}
}
});
Map<int, Contact> pathContacts = {};
// 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);
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;
}
}
});
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;
final target = widget.targetContact;
if (target != null) {
if (target.hasLocation) {
targetPos = LatLng(target.latitude!, target.longitude!);
} else if (pathData.isNotEmpty) {
// Infer from the last hop: average GPS contacts sharing that hop.
// For a round-trip path (flipPathRound), the target-side hop sits
// in the middle of the symmetric sequence; .last is the local side.
final lastHop = (widget.flipPathRound && pathData.length > 1)
? pathData[(pathData.length - 1) ~/ 2]
: pathData.last;
final peers = connector.contacts
.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 = (target.publicKey[1] / 255.0) * 2 * pi;
targetPos = LatLng(
lat + offsetDeg * cos(angle),
lon + offsetDeg * sin(angle),
);
targetGuessed = true;
}
}
}
_targetContactPosition = targetPos;
_targetContactIsGuessed = targetGuessed;
_points = <LatLng>[];
_points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
for (final hop in _traceData!.pathData) {
final contact = _traceData!.pathContacts[hop];
if (contact != null && contact.hasLocation) {
_points.add(LatLng(contact.latitude!, contact.longitude!));
} else {
final inferred = inferredPositions[hop];
if (inferred != null) _points.add(inferred);
}
}
if (targetPos != null) _points.add(targetPos);
_polylines = _points.length > 1
? [
Polyline(
points: _points,
strokeWidth: 4,
color: Colors.blueAccent,
),
]
: <Polyline>[];
_initialCenter = _points.isNotEmpty
? _points.first
: const LatLng(0, 0);
_initialZoom = _points.isNotEmpty ? 13.0 : 2.0;
_bounds = _points.length > 1 ? LatLngBounds.fromPoints(_points) : null;
_mapKey = ValueKey(
'${context.l10n.pathTrace_you},${_formatPathPrefixes(_traceData!.pathData)}',
);
_pathDistanceMeters = getPathDistanceMeters(_points);
});
} catch (e) {
appLogger.error(
'Error handling trace response: $e',
tag: 'PathTraceMapScreen',
setState(() {
_isLoading = false;
_hasData = true;
_traceData = PathTraceData(
pathData: pathData,
snrData: snrData,
pathContacts: pathContacts,
);
if (mounted) {
setState(() {
_isLoading = false;
_failed2Loaded = true;
});
_points = <LatLng>[];
_points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
for (final hop in _traceData!.pathData) {
final contact = _traceData!.pathContacts[hop];
if (contact != null &&
contact.hasLocation &&
contact.latitude != null &&
contact.longitude != null) {
_points.add(LatLng(contact.latitude!, contact.longitude!));
} else {
_noLocationErr = true;
}
}
}
_polylines = _points.length > 1
? [
Polyline(
points: _points,
strokeWidth: 4,
color: Colors.blueAccent,
),
]
: <Polyline>[];
_initialCenter = _points.isNotEmpty ? _points.first : const LatLng(0, 0);
_initialZoom = _points.isNotEmpty ? 13.0 : 2.0;
_bounds = _points.length > 1 ? LatLngBounds.fromPoints(_points) : null;
_mapKey = ValueKey(
'${context.l10n.pathTrace_you},${_formatPathPrefixes(_traceData!.pathData)}',
);
_pathDistance = getPathDistance();
});
}
@override
Widget build(BuildContext context) {
return Consumer<MeshCoreConnector>(
builder: (context, connector, _) {
final settings = context.watch<AppSettingsService>().settings;
final isImperial = settings.unitSystem == UnitSystem.imperial;
final tileCache = context.read<MapTileCacheService>();
return Scaffold(
@@ -439,7 +279,20 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
top: false,
child: Stack(
children: [
if (!_hasData)
if (_noLocationErr)
Center(
child: Card(
color: Colors.red,
child: Padding(
padding: EdgeInsets.all(12),
child: Text(
context.l10n.pathTrace_someHopsNoLocation,
style: TextStyle(color: Colors.white),
),
),
),
),
if (!_hasData && !_noLocationErr)
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -451,11 +304,43 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
],
),
),
if (_hasData) _buildMapPathTrace(context, tileCache),
if (_hasData && !_noLocationErr)
FlutterMap(
key: _mapKey,
options: MapOptions(
initialCenter: _initialCenter!,
initialZoom: _initialZoom,
initialCameraFit: _bounds == null
? null
: CameraFit.bounds(
bounds: _bounds!,
padding: const EdgeInsets.all(64),
maxZoom: 16,
),
minZoom: 2.0,
maxZoom: 18.0,
),
children: [
TileLayer(
urlTemplate: kMapTileUrlTemplate,
tileProvider: tileCache.tileProvider,
userAgentPackageName:
MapTileCacheService.userAgentPackageName,
maxZoom: 19,
),
if (_polylines.isNotEmpty)
PolylineLayer(polylines: _polylines),
if (_traceData!.pathData.isNotEmpty)
MarkerLayer(
markers: _buildHopMarkers(_traceData!.pathData),
),
],
),
if (_points.isEmpty &&
!_hasData &&
!_isLoading &&
!_failed2Loaded)
!_failed2Loaded &&
!_noLocationErr)
Center(
child: Card(
color: Colors.white.withValues(alpha: 0.9),
@@ -467,8 +352,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
),
),
),
if (_hasData)
_buildLegendCard(context, _traceData!, isImperial),
if (_hasData && !_noLocationErr)
_buildLegendCard(context, _traceData!),
],
),
),
@@ -477,70 +362,54 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
);
}
List<Marker> _buildHopMarkers(
List<int> pathData, {
required bool showLabels,
}) {
final markers = <Marker>[];
for (final hop in pathData) {
final contact = _traceData!.pathContacts[hop];
final inferred = _inferredHopPositions[hop];
final hasGps = contact != null && contact.hasLocation;
if (!hasGps && inferred == null) continue;
final point = hasGps
? LatLng(contact.latitude!, contact.longitude!)
: inferred!;
final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase();
markers.add(
Marker(
point: point,
width: 35,
height: 35,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: hasGps
? Colors.green
: Colors.orange.withValues(alpha: 0.75),
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),
),
],
List<Marker> _buildHopMarkers(List<int> pathData) {
return [
for (final hop in pathData)
if (_traceData!.pathContacts[hop]!.hasLocation)
Marker(
point: LatLng(
_traceData!.pathContacts[hop]!.latitude!,
_traceData!.pathContacts[hop]!.longitude!,
),
alignment: Alignment.center,
child: Text(
hasGps ? label : '~$label',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
width: 35,
height: 35,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.green,
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: Text(
_traceData!.pathContacts[hop]!.publicKey
.sublist(0, 1)
.map(
(b) => b.toRadixString(16).padLeft(2, '0').toUpperCase(),
)
.join(),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
),
),
);
if (showLabels) {
markers.add(
_buildNodeLabelMarker(
point: point,
label: contact?.name ?? '~$label',
),
);
}
}
final selfLat = context.read<MeshCoreConnector>().selfLatitude;
final selfLon = context.read<MeshCoreConnector>().selfLongitude;
if (selfLat != null && selfLon != null) {
final selfPoint = LatLng(selfLat, selfLon);
markers.add(
if (context.read<MeshCoreConnector>().selfLatitude != null &&
context.read<MeshCoreConnector>().selfLongitude != null)
Marker(
point: selfPoint,
point: LatLng(
context.read<MeshCoreConnector>().selfLatitude!,
context.read<MeshCoreConnector>().selfLongitude!,
),
width: 35,
height: 35,
child: Container(
@@ -568,94 +437,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
),
),
),
);
if (showLabels) {
markers.add(
_buildNodeLabelMarker(
point: selfPoint,
label: context.l10n.pathTrace_you,
),
);
}
}
// Add target contact endpoint marker.
final targetPos = _targetContactPosition;
if (targetPos != null) {
final isGuessed = _targetContactIsGuessed;
final targetName = widget.targetContact?.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;
}
Marker _buildNodeLabelMarker({required LatLng point, required String label}) {
return Marker(
point: point,
width: 120,
height: 24,
alignment: Alignment.topCenter,
child: IgnorePointer(
child: Transform.translate(
offset: const Offset(0, -20),
child: FittedBox(
fit: BoxFit.contain,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
),
),
),
),
);
];
}
String formatDirectionText(PathTraceData pathTraceData, int index) {
@@ -671,9 +453,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
.toRadixString(16)
.padLeft(2, '0')
.toUpperCase();
return contactName != null
? "$hex: $contactName"
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
return contactName != null ? "$hex: $contactName" : hex;
}
} else {
final contactName =
@@ -682,9 +462,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
.toRadixString(16)
.padLeft(2, '0')
.toUpperCase();
return contactName != null
? "$hex: $contactName"
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
return contactName != null ? "$hex: $contactName" : hex;
}
}
@@ -697,9 +475,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
.toRadixString(16)
.padLeft(2, '0')
.toUpperCase();
return contactName != null
? "$hex: $contactName"
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
return contactName != null ? "$hex: $contactName" : hex;
} else {
return context.l10n.pathTrace_you;
}
@@ -710,64 +486,11 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
.toRadixString(16)
.padLeft(2, '0')
.toUpperCase();
return contactName != null
? "$hex: $contactName"
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
return contactName != null ? "$hex: $contactName" : hex;
}
}
Widget _buildMapPathTrace(
BuildContext context,
MapTileCacheService tileCache,
) {
return FlutterMap(
key: _mapKey,
options: MapOptions(
interactionOptions: InteractionOptions(flags: ~InteractiveFlag.rotate),
initialCenter: _initialCenter!,
initialZoom: _initialZoom,
initialCameraFit: _bounds == null
? null
: CameraFit.bounds(
bounds: _bounds!,
padding: const EdgeInsets.all(64),
maxZoom: 16,
),
minZoom: 2.0,
maxZoom: 18.0,
onPositionChanged: (camera, hasGesture) {
final shouldShow = camera.zoom >= _labelZoomThreshold;
if (shouldShow != _showNodeLabels && mounted) {
setState(() {
_showNodeLabels = shouldShow;
});
}
},
),
children: [
TileLayer(
urlTemplate: kMapTileUrlTemplate,
tileProvider: tileCache.tileProvider,
userAgentPackageName: MapTileCacheService.userAgentPackageName,
maxZoom: 19,
),
if (_polylines.isNotEmpty) PolylineLayer(polylines: _polylines),
if (_traceData!.pathData.isNotEmpty)
MarkerLayer(
markers: _buildHopMarkers(
_traceData!.pathData,
showLabels: _showNodeLabels,
),
),
],
);
}
Widget _buildLegendCard(
BuildContext context,
PathTraceData pathTraceData,
bool isImperial,
) {
Widget _buildLegendCard(BuildContext context, PathTraceData pathTraceData) {
final l10n = context.l10n;
final maxHeight = MediaQuery.of(context).size.height * 0.35;
final estimatedHeight = 72.0 + (pathTraceData.pathData.length * 56.0);
@@ -786,7 +509,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
Padding(
padding: const EdgeInsets.all(12),
child: Text(
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters, isImperial: isImperial)}',
'${l10n.channelPath_repeaterHops} (${(_pathDistance / 1609.34).toStringAsFixed(2)} Miles / ${(_pathDistance / 1000).toStringAsFixed(2)} Km)',
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
@@ -800,14 +523,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
child: ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 4),
itemCount: pathTraceData.pathData.length + 1,
separatorBuilder: (_, _) => const Divider(height: 1),
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final snrUi = snrUiFromSNR(
index < pathTraceData.snrData.length
? pathTraceData.snrData[index]
: null,
context.read<MeshCoreConnector>().currentSf,
);
return Column(
children: [
ListTile(
@@ -826,22 +543,12 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
),
style: const TextStyle(fontSize: 14),
),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
snrUi.icon,
color: snrUi.color,
size: 18.0,
),
Text(
snrUi.text,
style: TextStyle(
fontSize: 10,
color: snrUi.color,
),
),
],
trailing: SNRIcon(
snr:
pathTraceData.snrData[index].toSigned(
8,
) /
4.0,
),
onTap: () {
// Handle item tap
-1
View File
@@ -168,7 +168,6 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
_commandController.clear();
_historyIndex = -1;
_commandFocusNode.requestFocus();
// Auto-scroll to bottom
Future.delayed(const Duration(milliseconds: 100), () {
+7 -67
View File
@@ -1,14 +1,12 @@
import 'package:flutter/material.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart';
import 'package:provider/provider.dart';
import '../l10n/l10n.dart';
import '../models/contact.dart';
import '../services/app_settings_service.dart';
import 'repeater_status_screen.dart';
import 'repeater_cli_screen.dart';
import 'repeater_settings_screen.dart';
import 'telemetry_screen.dart';
import 'neighbors_screen.dart';
import 'neighbours_screen.dart';
class RepeaterHubScreen extends StatelessWidget {
final Contact repeater;
@@ -23,10 +21,6 @@ class RepeaterHubScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final settingsService = context.watch<AppSettingsService>();
final chemistry = settingsService.batteryChemistryForRepeater(
repeater.publicKeyHex,
);
return Scaffold(
appBar: AppBar(
title: Column(
@@ -113,62 +107,6 @@ 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,
),
),
),
],
),
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),
),
DropdownMenuItem(
value: 'lifepo4',
child: Text(l10n.appSettings_batteryLifepo4),
),
DropdownMenuItem(
value: 'lipo',
child: Text(l10n.appSettings_batteryLipo),
),
],
),
],
),
),
),
const SizedBox(height: 24),
Text(
l10n.repeater_managementTools,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
@@ -236,15 +174,17 @@ class RepeaterHubScreen extends StatelessWidget {
_buildManagementCard(
context,
icon: Icons.group,
title: l10n.repeater_neighbors,
subtitle: l10n.repeater_neighborsSubtitle,
title: l10n.repeater_neighbours,
subtitle: l10n.repeater_neighboursSubtitle,
color: Colors.orange,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
NeighborsScreen(repeater: repeater, password: password),
builder: (context) => NeighboursScreen(
repeater: repeater,
password: password,
),
),
);
},
+9 -35
View File
@@ -8,9 +8,7 @@ import '../models/contact.dart';
import '../models/path_selection.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../services/app_settings_service.dart';
import '../services/repeater_command_service.dart';
import '../utils/battery_utils.dart';
import '../widgets/path_management_dialog.dart';
class RepeaterStatusScreen extends StatefulWidget {
@@ -181,12 +179,6 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
_dupDirect = directDups;
_dupFlood = floodDups;
});
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
connector.updateRepeaterBatterySnapshot(
widget.repeater.publicKeyHex,
batteryMv,
source: 'status_binary',
);
_recordStatusResult(true);
}
@@ -209,18 +201,6 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
_uptimeSecs = _asInt(data['uptime_secs']);
_queueLen = _asInt(data['queue_len']);
_debugFlags = _asInt(data['errors']);
final batteryMv = _batteryMv;
if (batteryMv != null) {
final connector = Provider.of<MeshCoreConnector>(
context,
listen: false,
);
connector.updateRepeaterBatterySnapshot(
widget.repeater.publicKeyHex,
batteryMv,
source: 'status_text',
);
}
} else if (data.containsKey('noise_floor')) {
_noiseFloor = _asInt(data['noise_floor']);
_lastRssi = _asInt(data['last_rssi']);
@@ -610,24 +590,18 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
}
String _batteryText() {
final connector = context.watch<MeshCoreConnector>();
final batteryMv =
connector.getRepeaterBatteryMillivolts(widget.repeater.publicKeyHex) ??
_batteryMv;
if (batteryMv == null) return '';
final percent = estimateBatteryPercentFromMillivolts(
batteryMv,
_batteryChemistry(),
);
final volts = (batteryMv / 1000.0).toStringAsFixed(2);
if (_batteryMv == null) return '';
final percent = _batteryPercentFromMv(_batteryMv!);
final volts = (_batteryMv! / 1000.0).toStringAsFixed(2);
return '$percent% / ${volts}V';
}
String _batteryChemistry() {
final settingsService = context.read<AppSettingsService>();
return settingsService.batteryChemistryForRepeater(
widget.repeater.publicKeyHex,
);
int _batteryPercentFromMv(int millivolts) {
const minMv = 3000;
const maxMv = 4200;
if (millivolts <= minMv) return 0;
if (millivolts >= maxMv) return 100;
return (((millivolts - minMv) * 100) / (maxMv - minMv)).round();
}
String _clockText() {
+29 -171
View File
@@ -1,17 +1,11 @@
import 'dart:async';
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 '../utils/app_logger.dart';
import '../widgets/adaptive_app_bar_title.dart';
import '../widgets/device_tile.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 {
@@ -23,23 +17,17 @@ 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;
@override
void initState() {
super.initState();
_connector = Provider.of<MeshCoreConnector>(context, listen: false);
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
_connectionListener = () {
final isCurrentRoute = ModalRoute.of(context)?.isCurrent ?? true;
if (_connector.state == MeshCoreConnectionState.disconnected) {
if (connector.state == MeshCoreConnectionState.disconnected) {
_changedNavigation = false;
} else if (_connector.state == MeshCoreConnectionState.connected &&
_connector.activeTransport == MeshCoreTransportType.bluetooth &&
isCurrentRoute &&
} else if (connector.state == MeshCoreConnectionState.connected &&
!_changedNavigation) {
_changedNavigation = true;
if (mounted) {
@@ -50,53 +38,21 @@ class _ScannerScreenState extends State<ScannerScreen> {
}
};
_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());
}
}
},
onError: (Object e) {
appLogger.warn('Adapter state stream error: $e', tag: 'ScannerScreen');
},
);
connector.addListener(_connectionListener);
}
@override
void dispose() {
_connector.removeListener(_connectionListener);
unawaited(_bluetoothStateSubscription.cancel());
if (!_changedNavigation) {
WidgetsBinding.instance.addPostFrameCallback((_) {
unawaited(_connector.disconnect(manual: true));
});
}
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
connector.removeListener(_connectionListener);
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),
title: Text(context.l10n.scanner_title),
centerTitle: true,
automaticallyImplyLeading: false,
),
@@ -106,10 +62,6 @@ class _ScannerScreenState extends State<ScannerScreen> {
builder: (context, connector, child) {
return Column(
children: [
// Bluetooth off warning
if (_bluetoothState == BluetoothAdapterState.off)
_bluetoothOffWarning(context),
// Status bar
_buildStatusBar(context, connector),
@@ -120,84 +72,33 @@ class _ScannerScreenState extends State<ScannerScreen> {
},
),
),
bottomNavigationBar: Consumer<MeshCoreConnector>(
floatingActionButton: 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 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),
return FloatingActionButton.extended(
onPressed: () {
if (isScanning) {
connector.stopScan();
} else {
connector.startScan();
}
},
icon: isScanning
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
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,
),
),
],
),
)
: const Icon(Icons.bluetooth_searching),
label: Text(
isScanning
? context.l10n.scanner_stop
: context.l10n.scanner_scan,
),
);
},
@@ -304,47 +205,4 @@ class _ScannerScreenState extends State<ScannerScreen> {
}
}
}
Widget _bluetoothOffWarning(BuildContext context) {
final errorColor = Theme.of(context).colorScheme.error;
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
color: errorColor.withValues(alpha: 0.15),
child: Row(
children: [
Icon(Icons.bluetooth_disabled, size: 24, color: errorColor),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.scanner_bluetoothOff,
style: TextStyle(
color: errorColor,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
const SizedBox(height: 4),
Text(
context.l10n.scanner_bluetoothOffMessage,
style: TextStyle(
color: errorColor.withValues(alpha: 0.85),
fontSize: 12,
),
),
],
),
),
if (PlatformInfo.isAndroid)
TextButton(
onPressed: () => FlutterBluePlus.turnOn(),
child: Text(context.l10n.scanner_enableBluetooth),
),
],
),
);
}
}
+62 -185
View File
@@ -8,7 +8,6 @@ import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../l10n/l10n.dart';
import '../models/radio_settings.dart';
import '../widgets/app_bar.dart';
import 'app_settings_screen.dart';
import 'app_debug_log_screen.dart';
import 'ble_debug_log_screen.dart';
@@ -42,13 +41,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
Widget build(BuildContext context) {
final l10n = context.l10n;
return Scaffold(
appBar: AppBar(
title: AppBarTitle(
l10n.settings_title,
indicators: false,
subtitle: false,
),
),
appBar: AppBar(title: Text(l10n.settings_title), centerTitle: true),
body: SafeArea(
top: false,
child: Consumer<MeshCoreConnector>(
@@ -277,14 +270,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
onTap: () => _editLocation(context, connector),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.group_add_outlined),
title: Text(l10n.settings_contactSettings),
subtitle: Text(l10n.settings_contactSettingsSubtitle),
trailing: const Icon(Icons.chevron_right),
onTap: () => _editAutoAddConfig(context, connector),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.visibility_off_outlined),
title: Text(l10n.settings_privacyMode),
@@ -505,6 +490,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
final latController = TextEditingController();
final lonController = TextEditingController();
final intervalController = TextEditingController();
bool isLogging = connector.sparseLocationLogger?.isLogging() ?? false;
latController.text = connector.selfLatitude?.toStringAsFixed(6) ?? '';
lonController.text = connector.selfLongitude?.toStringAsFixed(6) ?? '';
@@ -549,6 +536,20 @@ class _SettingsScreenState extends State<SettingsScreen> {
signed: true,
),
),
const SizedBox(height: 16),
FeatureToggleRow(
title: "GPS Logging",
subtitle: "Enable GPS logging on the device",
value: isLogging,
onChanged: (value) async {
setDialogState(() => isLogging = value);
if (value) {
await connector.sparseLocationLogger?.startLogging();
} else {
await connector.sparseLocationLogger?.stopLogging();
}
},
),
if (hasGPS) ...[
const SizedBox(height: 16),
TextField(
@@ -757,7 +758,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
Future<void> _gpxExport(
_gpxExport(
GpxExport exporter,
String name,
String description,
@@ -797,7 +798,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
}
}
Widget _buildExportCard(MeshCoreConnector connector) {
_buildExportCard(MeshCoreConnector connector) {
final l10n = context.l10n;
return Card(
child: Column(
@@ -860,121 +861,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
);
}
void _editAutoAddConfig(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
bool autoAddChat = false;
bool autoAddRepeater = false;
bool autoAddRoomServer = false;
bool autoAddSensor = false;
bool overwriteOldest = false;
final connector = context.read<MeshCoreConnector>();
autoAddChat = connector.autoAddUsers ?? false;
autoAddRepeater = connector.autoAddRepeaters ?? false;
autoAddRoomServer = connector.autoAddRoomServers ?? false;
autoAddSensor = connector.autoAddSensors ?? false;
overwriteOldest = connector.autoAddOverwriteOldest ?? false;
showDialog(
context: context,
builder: (dialogContext) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: Text(l10n.contactsSettings_autoAddTitle),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
FeatureToggleRow(
title: l10n.contactsSettings_autoAddUsersTitle,
subtitle: l10n.contactsSettings_autoAddUsersSubtitle,
value: autoAddChat,
onChanged: (value) {
setDialogState(() => autoAddChat = value);
},
),
SizedBox(height: 8),
FeatureToggleRow(
title: l10n.contactsSettings_autoAddRepeatersTitle,
subtitle: l10n.contactsSettings_autoAddRepeatersSubtitle,
value: autoAddRepeater,
onChanged: (value) {
setDialogState(() => autoAddRepeater = value);
},
),
SizedBox(height: 8),
FeatureToggleRow(
title: l10n.contactsSettings_autoAddRoomServersTitle,
subtitle: l10n.contactsSettings_autoAddRoomServersSubtitle,
value: autoAddRoomServer,
onChanged: (value) {
setDialogState(() => autoAddRoomServer = value);
},
),
SizedBox(height: 8),
FeatureToggleRow(
title: l10n.contactsSettings_autoAddSensorsTitle,
subtitle: l10n.contactsSettings_autoAddSensorsSubtitle,
value: autoAddSensor,
onChanged: (value) {
setDialogState(() => autoAddSensor = value);
},
),
Divider(height: 4),
FeatureToggleRow(
title: l10n.contactsSettings_overwriteOldestTitle,
subtitle: l10n.contactsSettings_overwriteOldestSubtitle,
value: overwriteOldest,
onChanged: (value) {
setDialogState(() => overwriteOldest = value);
},
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
TextButton(
onPressed: () {
_sendSettings(
connector,
autoAddChat,
autoAddRepeater,
autoAddRoomServer,
autoAddSensor,
overwriteOldest,
);
Navigator.pop(context);
},
child: Text(l10n.common_save),
),
],
),
),
);
}
void _sendSettings(
MeshCoreConnector connector,
bool autoAddChat,
bool autoAddRepeater,
bool autoAddRoomServer,
bool autoAddSensor,
bool overwriteOldest,
) async {
final frame = buildSetAutoAddConfigFrame(
autoAddChat: autoAddChat,
autoAddRepeater: autoAddRepeater,
autoAddRoomServer: autoAddRoomServer,
autoAddSensor: autoAddSensor,
overwriteOldest: overwriteOldest,
);
await connector.sendFrame(frame);
await connector.sendFrame(buildGetAutoAddFlagsFrame());
}
}
class _RadioSettingsDialog extends StatefulWidget {
@@ -992,7 +878,6 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
LoRaSpreadingFactor _spreadingFactor = LoRaSpreadingFactor.sf7;
LoRaCodingRate _codingRate = LoRaCodingRate.cr4_5;
final _txPowerController = TextEditingController(text: '20');
bool _clientRepeat = false;
@override
void initState() {
@@ -1042,8 +927,6 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
if (widget.connector.currentTxPower != null) {
_txPowerController.text = widget.connector.currentTxPower.toString();
}
_clientRepeat = widget.connector.clientRepeat ?? false;
}
@override
@@ -1093,29 +976,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
widget.connector.currentCr,
);
// if the client repeat isnt null then we know its supported
//otherwise we leave it out of the frame to avoid accidentally enabling
final knownRepeat = widget.connector.clientRepeat != null;
if (knownRepeat) {
const validRepeatFreqsKHz = {433000, 869000, 918000};
if (_clientRepeat && !validRepeatFreqsKHz.contains(freqHz)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_clientRepeatFreqWarning)),
);
return;
}
}
try {
await widget.connector.sendFrame(
buildSetRadioParamsFrame(
freqHz,
bwHz,
sf,
cr,
clientRepeat: knownRepeat ? _clientRepeat : null,
),
buildSetRadioParamsFrame(freqHz, bwHz, sf, cr),
);
await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower));
await widget.connector.refreshDeviceInfo();
@@ -1154,25 +1017,37 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DropdownButtonFormField<int>(
decoration: InputDecoration(
labelText: l10n.settings_presets,
border: const OutlineInputBorder(),
),
items: [
for (var i = 0; i < RadioSettings.presets.length; i++)
DropdownMenuItem(
value: i,
child: Text(RadioSettings.presets[i].$1),
),
],
onChanged: (index) {
if (index != null) {
_applyPreset(RadioSettings.presets[index].$2);
}
},
Text(
l10n.settings_presets,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: [
_PresetChip(
label: l10n.settings_preset915Mhz,
onTap: () => _applyPreset(RadioSettings.preset915MHz),
),
_PresetChip(
label: l10n.settings_preset868Mhz,
onTap: () => _applyPreset(RadioSettings.preset868MHz),
),
_PresetChip(
label: l10n.settings_preset433Mhz,
onTap: () => _applyPreset(RadioSettings.preset433MHz),
),
_PresetChip(
label: l10n.settings_longRange,
onTap: () => _applyPreset(RadioSettings.presetLongRange),
),
_PresetChip(
label: l10n.settings_fastSpeed,
onTap: () => _applyPreset(RadioSettings.presetFastSpeed),
),
],
),
const SizedBox(height: 24),
TextField(
controller: _frequencyController,
decoration: InputDecoration(
@@ -1244,16 +1119,6 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
),
keyboardType: TextInputType.number,
),
if (widget.connector.clientRepeat != null) ...[
const SizedBox(height: 16),
SwitchListTile(
title: Text(l10n.settings_clientRepeat),
subtitle: Text(l10n.settings_clientRepeatSubtitle),
value: _clientRepeat,
onChanged: (value) => setState(() => _clientRepeat = value),
contentPadding: EdgeInsets.zero,
),
],
],
),
),
@@ -1267,3 +1132,15 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
);
}
}
class _PresetChip extends StatelessWidget {
final String label;
final VoidCallback onTap;
const _PresetChip({required this.label, required this.onTap});
@override
Widget build(BuildContext context) {
return ActionChip(label: Text(label), onPressed: onTap);
}
}
-282
View File
@@ -1,282 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../utils/platform_info.dart';
import '../widgets/adaptive_app_bar_title.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();
_portController = TextEditingController(text: '5000');
_connector = context.read<MeshCoreConnector>();
_connectionListener = () {
if (!mounted) return;
if (_connector.state == MeshCoreConnectionState.disconnected) {
_navigatedToContacts = false;
}
if (_connector.state == MeshCoreConnectionState.connected &&
_connector.isTcpTransportConnected &&
!_navigatedToContacts) {
_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;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(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());
}
}
+17 -49
View File
@@ -5,14 +5,11 @@ import 'package:provider/provider.dart';
import '../l10n/l10n.dart';
import '../models/contact.dart';
import '../models/path_selection.dart';
import '../models/app_settings.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../services/app_settings_service.dart';
import '../services/repeater_command_service.dart';
import '../widgets/path_management_dialog.dart';
import '../helpers/cayenne_lpp.dart';
import '../utils/battery_utils.dart';
class TelemetryScreen extends StatefulWidget {
final Contact repeater;
@@ -75,19 +72,9 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
}
void _handleStatusResponse(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,
batteryMv,
source: 'telemetry',
);
}
if (!mounted) return;
setState(() {
_parsedTelemetry = parsedTelemetry;
_parsedTelemetry = CayenneLpp.parseByChannel(frame);
});
ScaffoldMessenger.of(context).showSnackBar(
@@ -194,8 +181,6 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
Widget build(BuildContext context) {
final l10n = context.l10n;
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;
@@ -322,7 +307,6 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
entry['values'],
l10n.telemetry_channelTitle(entry['channel']),
entry['channel'],
isImperialUnits,
),
],
),
@@ -335,7 +319,6 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
Map<String, dynamic> channelData,
String title,
int channel,
bool isImperialUnits,
) {
final l10n = context.l10n;
return Card(
@@ -375,12 +358,12 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
else if (entry.key == 'temperature' && channel == 1)
_buildInfoRow(
l10n.telemetry_mcuTemperatureLabel,
_temperatureText(entry.value, isImperialUnits),
_temperatureText(entry.value),
)
else if (entry.key == 'temperature')
_buildInfoRow(
l10n.telemetry_temperatureLabel,
_temperatureText(entry.value, isImperialUnits),
_temperatureText(entry.value),
)
else if (entry.key == 'current' && channel == 1)
_buildInfoRow(
@@ -422,44 +405,29 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
);
}
int? _extractTelemetryBatteryMillivolts(List<Map<String, dynamic>> entries) {
for (final entry in entries) {
if (entry['channel'] != 1) continue;
final values = entry['values'];
if (values is! Map<String, dynamic>) continue;
final voltage = values['voltage'];
if (voltage is num) return (voltage.toDouble() * 1000).round();
}
return null;
}
String _batteryText(double? telemetryVolts) {
String _batteryText(double? batteryMv) {
final l10n = context.l10n;
final connector = context.watch<MeshCoreConnector>();
final batteryMv =
connector.getRepeaterBatteryMillivolts(widget.repeater.publicKeyHex) ??
(telemetryVolts == null ? null : (telemetryVolts * 1000).round());
if (batteryMv == null) return l10n.common_notAvailable;
final chemistry = _batteryChemistry();
final percent = estimateBatteryPercentFromMillivolts(batteryMv, chemistry);
final volts = (batteryMv / 1000).toStringAsFixed(2);
final percent = _batteryPercentFromMv(batteryMv);
final volts = batteryMv.toStringAsFixed(2);
return l10n.telemetry_batteryValue(percent, volts);
}
String _batteryChemistry() {
final settingsService = context.read<AppSettingsService>();
return settingsService.batteryChemistryForRepeater(
widget.repeater.publicKeyHex,
);
int _batteryPercentFromMv(double millivolts) {
const minMv = 2.800;
const maxMv = 4.200;
if (millivolts <= minMv) return 0;
if (millivolts >= maxMv) return 100;
return (((millivolts - minMv) * 100) / (maxMv - minMv)).round();
}
String _temperatureText(double? tempC, bool isImperialUnits) {
String _temperatureText(double? tempC) {
final l10n = context.l10n;
if (tempC == null) return l10n.common_notAvailable;
final tempF = (tempC * 9 / 5) + 32;
if (isImperialUnits) {
return '${tempF.toStringAsFixed(1)}°F';
}
return '${tempC.toStringAsFixed(1)}°C';
return l10n.telemetry_temperatureValue(
tempC.toStringAsFixed(1),
tempF.toStringAsFixed(1),
);
}
}
-441
View File
@@ -1,441 +0,0 @@
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 '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;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
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();
}
}
+1 -6
View File
@@ -52,12 +52,7 @@ class AppDebugLogService extends ChangeNotifier {
String tag = 'App',
AppDebugLogLevel level = AppDebugLogLevel.info,
}) {
if (!_enabled && !kDebugMode) return;
if (!_enabled) {
// In debug mode, still print to console but don't store entries.
debugPrint('[$tag] $message');
return;
}
if (!_enabled) return;
_entries.add(
AppDebugLogEntry(
-50
View File
@@ -17,12 +17,6 @@ class AppSettingsService extends ChangeNotifier {
return stored ?? 'nmc';
}
String batteryChemistryForRepeater(String repeaterPubKeyHex) {
final stored = _settings.batteryChemistryByRepeaterId[repeaterPubKeyHex];
if (stored == 'liion') return 'nmc';
return stored ?? 'nmc';
}
Future<void> loadSettings() async {
final prefs = PrefsManager.instance;
final jsonStr = prefs.getString(_settingsKey);
@@ -80,14 +74,6 @@ 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));
}
Future<void> setMapCacheBounds(Map<String, double>? value) async {
await updateSettings(_settings.copyWith(mapCacheBounds: value));
}
@@ -134,10 +120,6 @@ 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,
@@ -150,36 +132,4 @@ class AppSettingsService extends ChangeNotifier {
_settings.copyWith(batteryChemistryByDeviceId: updated),
);
}
Future<void> setBatteryChemistryForRepeater(
String repeaterPubKeyHex,
String chemistry,
) async {
final updated = Map<String, String>.from(
_settings.batteryChemistryByRepeaterId,
);
updated[repeaterPubKeyHex] = chemistry;
await updateSettings(
_settings.copyWith(batteryChemistryByRepeaterId: updated),
);
}
Future<void> setUnitSystem(UnitSystem value) async {
await updateSettings(_settings.copyWith(unitSystem: value));
}
bool isChannelMuted(String channelName) {
return _settings.mutedChannels.contains(channelName);
}
Future<void> muteChannel(String channelName) async {
final updated = Set<String>.from(_settings.mutedChannels)..add(channelName);
await updateSettings(_settings.copyWith(mutedChannels: updated));
}
Future<void> unmuteChannel(String channelName) async {
final updated = Set<String>.from(_settings.mutedChannels)
..remove(channelName);
await updateSettings(_settings.copyWith(mutedChannels: updated));
}
}
+16 -9
View File
@@ -1,11 +1,13 @@
import '../utils/platform_info.dart';
import 'dart:isolate';
import 'dart:io';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
class BackgroundService {
bool _initialized = false;
Future<void> initialize() async {
if (!PlatformInfo.isAndroid || _initialized) return;
if (!Platform.isAndroid || _initialized) return;
FlutterForegroundTask.init(
androidNotificationOptions: AndroidNotificationOptions(
channelId: 'meshcore_background',
@@ -13,13 +15,18 @@ class BackgroundService {
channelDescription: 'Keeps MeshCore running in the background.',
channelImportance: NotificationChannelImportance.LOW,
priority: NotificationPriority.LOW,
iconData: const NotificationIconData(
resType: ResourceType.mipmap,
resPrefix: ResourcePrefix.ic,
name: 'launcher',
),
),
iosNotificationOptions: const IOSNotificationOptions(
showNotification: false,
playSound: false,
),
foregroundTaskOptions: ForegroundTaskOptions(
eventAction: ForegroundTaskEventAction.repeat(5000),
foregroundTaskOptions: const ForegroundTaskOptions(
interval: 5000,
autoRunOnBoot: false,
allowWifiLock: false,
),
@@ -28,7 +35,7 @@ class BackgroundService {
}
Future<void> start() async {
if (!PlatformInfo.isAndroid) return;
if (!Platform.isAndroid) return;
if (!_initialized) {
await initialize();
}
@@ -42,7 +49,7 @@ class BackgroundService {
}
Future<void> stop() async {
if (!PlatformInfo.isAndroid) return;
if (!Platform.isAndroid) return;
final running = await FlutterForegroundTask.isRunningService;
if (!running) return;
await FlutterForegroundTask.stopService();
@@ -56,13 +63,13 @@ void startCallback() {
class _MeshCoreTaskHandler extends TaskHandler {
@override
Future<void> onStart(DateTime timestamp, TaskStarter starter) async {}
void onStart(DateTime timestamp, SendPort? sendPort) {}
@override
void onRepeatEvent(DateTime timestamp) {}
void onRepeatEvent(DateTime timestamp, SendPort? sendPort) {}
@override
Future<void> onDestroy(DateTime timestamp, bool isTimeout) async {}
void onDestroy(DateTime timestamp, SendPort? sendPort) {}
@override
void onNotificationButtonPressed(String id) {}
+6 -24
View File
@@ -1,5 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import '../connector/meshcore_protocol.dart';
class BleDebugLogEntry {
@@ -45,7 +44,6 @@ class BleDebugLogService extends ChangeNotifier {
static const int maxEntries = 500;
final List<BleDebugLogEntry> _entries = [];
final List<BleRawLogRxEntry> _rawLogRxEntries = [];
bool _notifyScheduled = false;
List<BleDebugLogEntry> get entries => List.unmodifiable(_entries);
List<BleRawLogRxEntry> get rawLogRxEntries =>
@@ -80,31 +78,13 @@ class BleDebugLogService extends ChangeNotifier {
}
}
_notifyListenersSafely();
notifyListeners();
}
void clear() {
_entries.clear();
_rawLogRxEntries.clear();
_notifyListenersSafely();
}
void _notifyListenersSafely() {
final phase = SchedulerBinding.instance.schedulerPhase;
final canNotifyNow =
phase == SchedulerPhase.idle ||
phase == SchedulerPhase.postFrameCallbacks;
if (canNotifyNow) {
notifyListeners();
return;
}
if (_notifyScheduled) return;
_notifyScheduled = true;
SchedulerBinding.instance.addPostFrameCallback((_) {
_notifyScheduled = false;
notifyListeners();
});
notifyListeners();
}
String _describeFrame(
@@ -172,6 +152,8 @@ 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:
@@ -213,8 +195,8 @@ class BleDebugLogService extends ChangeNotifier {
return 'RESP_CODE_CHANNEL_MSG_RECV_V3';
case respCodeChannelInfo:
return 'RESP_CODE_CHANNEL_INFO';
case respCodeAutoAddConfig:
return 'RESP_CODE_AUTO_ADD_CONFIG';
case respCodeRadioSettings:
return 'RESP_CODE_RADIO_SETTINGS';
case pushCodeTraceData:
return 'PUSH_CODE_TRACE_DATA';
default:
-72
View File
@@ -1,72 +0,0 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import '../storage/prefs_manager.dart';
/// Client-side accessibility/UI service that exposes a persistent shared text scale
/// factor. No MeshCoreConnector/RoomServer or protocol interaction occurs, and the
/// value is saved locally via SharedPreferences so it can be reused in Markdown
/// viewers, log panels, or other text-heavy widgets without redundant network
/// dependencies.
///
/// Widgets should scope rebuilds using the snippet below so only the scaled text
/// is rebuilt instead of the entire chat list:
/// ```dart
/// context.select<ChatTextScaleService, double>(
/// (service) => service.scale,
/// )
/// ```
class ChatTextScaleService extends ChangeNotifier {
static const _prefKey = 'chat_text_scale';
static const double _minScale = 0.8;
static const double _maxScale = 1.8;
double _scale = 1.0;
Timer? _saveTimer;
double get scale => _scale;
Future<void> initialize() async {
final stored = PrefsManager.instance.getDouble(_prefKey);
if (stored != null) {
_scale = _clamp(stored);
}
}
void setScale(double value, {bool persistImmediately = false}) {
final next = _clamp(value);
if (next == _scale) return;
_scale = next;
notifyListeners();
if (persistImmediately) {
_commitScale();
} else {
_scheduleSave();
}
}
void reset() {
setScale(1.0, persistImmediately: true);
}
void persist() => _commitScale();
@override
void dispose() {
_saveTimer?.cancel();
super.dispose();
}
void _scheduleSave() {
_saveTimer?.cancel();
_saveTimer = Timer(const Duration(milliseconds: 250), _commitScale);
}
void _commitScale() {
_saveTimer?.cancel();
PrefsManager.instance.setDouble(_prefKey, _scale);
}
double _clamp(double value) => value.clamp(_minScale, _maxScale).toDouble();
}
-446
View File
@@ -1,446 +0,0 @@
import 'dart:convert';
import 'dart:async';
import 'dart:math';
import 'package:http/http.dart' as http;
import 'package:latlong2/latlong.dart';
typedef ElevationDataSource =
Future<List<double?>> Function(List<LatLng> points);
class LineOfSightSample {
final double distanceMeters;
final double terrainMeters;
final double lineHeightMeters;
final double refractedHeightMeters;
final double clearanceMeters;
const LineOfSightSample({
required this.distanceMeters,
required this.terrainMeters,
required this.lineHeightMeters,
required this.refractedHeightMeters,
required this.clearanceMeters,
});
}
class LineOfSightResult {
final bool hasData;
final bool isClear;
final double totalDistanceMeters;
final double maxObstructionMeters;
final double? firstObstructionDistanceMeters;
final List<LineOfSightSample> samples;
final String? errorMessage;
final double usedKFactor;
final double? frequencyMHz;
const LineOfSightResult({
required this.hasData,
required this.isClear,
required this.totalDistanceMeters,
required this.maxObstructionMeters,
required this.firstObstructionDistanceMeters,
required this.samples,
required this.usedKFactor,
this.frequencyMHz,
this.errorMessage,
});
const LineOfSightResult.error({
required this.totalDistanceMeters,
required this.errorMessage,
this.usedKFactor = 4.0 / 3.0,
this.frequencyMHz,
}) : hasData = false,
isClear = false,
maxObstructionMeters = 0,
firstObstructionDistanceMeters = null,
samples = const [];
}
class LineOfSightPathSegment {
final int index;
final LatLng start;
final LatLng end;
final LineOfSightResult result;
const LineOfSightPathSegment({
required this.index,
required this.start,
required this.end,
required this.result,
});
}
class LineOfSightPathResult {
final List<LineOfSightPathSegment> segments;
final int clearSegments;
final int blockedSegments;
final int unknownSegments;
const LineOfSightPathResult({
required this.segments,
required this.clearSegments,
required this.blockedSegments,
required this.unknownSegments,
});
}
class LineOfSightService {
static const String errorElevationUnavailable =
'los_error_elevation_unavailable';
static const String errorInvalidInput = 'los_error_invalid_input';
static const double _earthRadiusMeters = 6371000.0;
static const Distance _distance = Distance();
static const Duration _cacheTtl = Duration(hours: 24);
static const int _maxFetchAttempts = 4; // initial try + 3 retries
static const Duration _initialBackoff = Duration(milliseconds: 300);
static const double _baselineFrequencyMHz = 915.0;
static const double _baselineKFactor = 4.0 / 3.0;
static double get baselineFrequencyMHz => _baselineFrequencyMHz;
static double get baselineKFactor => _baselineKFactor;
final http.Client _httpClient;
final bool _ownsHttpClient;
final ElevationDataSource? _elevationDataSource;
final Map<String, _CachedElevation> _elevationCache = {};
LineOfSightService({
http.Client? httpClient,
ElevationDataSource? elevationDataSource,
}) : _httpClient = httpClient ?? http.Client(),
_ownsHttpClient = httpClient == null,
_elevationDataSource = elevationDataSource;
Future<LineOfSightPathResult> analyzePath(
List<LatLng> points, {
double startAntennaHeightMeters = 1.5,
double endAntennaHeightMeters = 1.5,
double? frequencyMHz,
double obstructionToleranceMeters = 0.0,
}) async {
if (points.length < 2) {
return const LineOfSightPathResult(
segments: [],
clearSegments: 0,
blockedSegments: 0,
unknownSegments: 0,
);
}
final segments = <LineOfSightPathSegment>[];
var clearSegments = 0;
var blockedSegments = 0;
var unknownSegments = 0;
final kFactor = _kFactorForFrequency(frequencyMHz);
for (int i = 0; i < points.length - 1; i++) {
final result = await analyzeLink(
points[i],
points[i + 1],
startAntennaHeightMeters: startAntennaHeightMeters,
endAntennaHeightMeters: endAntennaHeightMeters,
kFactor: kFactor,
frequencyMHz: frequencyMHz,
obstructionToleranceMeters: obstructionToleranceMeters,
);
segments.add(
LineOfSightPathSegment(
index: i,
start: points[i],
end: points[i + 1],
result: result,
),
);
if (!result.hasData) {
unknownSegments++;
} else if (result.isClear) {
clearSegments++;
} else {
blockedSegments++;
}
}
return LineOfSightPathResult(
segments: segments,
clearSegments: clearSegments,
blockedSegments: blockedSegments,
unknownSegments: unknownSegments,
);
}
Future<LineOfSightResult> analyzeLink(
LatLng start,
LatLng end, {
double startAntennaHeightMeters = 1.5,
double endAntennaHeightMeters = 1.5,
required double kFactor,
double? frequencyMHz,
double obstructionToleranceMeters = 0.0,
}) async {
final totalDistanceMeters = _distance.as(LengthUnit.Meter, start, end);
if (totalDistanceMeters <= 1) {
return LineOfSightResult(
hasData: true,
isClear: true,
totalDistanceMeters: totalDistanceMeters,
maxObstructionMeters: 0,
firstObstructionDistanceMeters: null,
samples: const [],
usedKFactor: kFactor,
frequencyMHz: frequencyMHz,
);
}
final samplePoints = _buildSamplePoints(start, end, totalDistanceMeters);
final elevations = await _getElevations(samplePoints);
if (elevations.any((e) => e == null)) {
return LineOfSightResult.error(
totalDistanceMeters: totalDistanceMeters,
errorMessage: errorElevationUnavailable,
usedKFactor: kFactor,
frequencyMHz: frequencyMHz,
);
}
return computeFromElevations(
points: samplePoints,
elevations: elevations.cast<double>(),
startAntennaHeightMeters: startAntennaHeightMeters,
endAntennaHeightMeters: endAntennaHeightMeters,
kFactor: kFactor,
frequencyMHz: frequencyMHz,
obstructionToleranceMeters: obstructionToleranceMeters,
);
}
static LineOfSightResult computeFromElevations({
required List<LatLng> points,
required List<double> elevations,
double startAntennaHeightMeters = 1.5,
double endAntennaHeightMeters = 1.5,
required double kFactor,
double? frequencyMHz,
double obstructionToleranceMeters = 0.0,
}) {
if (points.length < 2 || elevations.length != points.length) {
return LineOfSightResult.error(
totalDistanceMeters: 0,
errorMessage: errorInvalidInput,
usedKFactor: kFactor,
frequencyMHz: frequencyMHz,
);
}
final totalDistanceMeters = _distance.as(
LengthUnit.Meter,
points.first,
points.last,
);
final effectiveEarthRadius = _earthRadiusMeters * kFactor;
final startLineHeight = elevations.first + startAntennaHeightMeters;
final endLineHeight = elevations.last + endAntennaHeightMeters;
var maxObstructionMeters = 0.0;
double? firstObstructionDistanceMeters;
final samples = <LineOfSightSample>[];
var isClear = true;
for (int i = 0; i < points.length; i++) {
final fraction = points.length == 1 ? 0.0 : i / (points.length - 1);
final distanceFromStart = totalDistanceMeters * fraction;
final lineHeight =
startLineHeight + (endLineHeight - startLineHeight) * fraction;
final earthBulge =
(distanceFromStart * (totalDistanceMeters - distanceFromStart)) /
(2 * effectiveEarthRadius);
final terrainHeight = elevations[i] + earthBulge;
final clearance = lineHeight - terrainHeight;
final unrefBulge =
(distanceFromStart * (totalDistanceMeters - distanceFromStart)) /
(2 * _earthRadiusMeters);
final refractedHeight = lineHeight + (unrefBulge - earthBulge);
if (clearance < -obstructionToleranceMeters) {
isClear = false;
final obstruction = -clearance;
if (obstruction > maxObstructionMeters) {
maxObstructionMeters = obstruction;
}
firstObstructionDistanceMeters ??= distanceFromStart;
}
samples.add(
LineOfSightSample(
distanceMeters: distanceFromStart,
terrainMeters: terrainHeight,
lineHeightMeters: lineHeight,
refractedHeightMeters: refractedHeight,
clearanceMeters: clearance,
),
);
}
return LineOfSightResult(
hasData: true,
isClear: isClear,
totalDistanceMeters: totalDistanceMeters,
maxObstructionMeters: maxObstructionMeters,
firstObstructionDistanceMeters: firstObstructionDistanceMeters,
samples: samples,
usedKFactor: kFactor,
frequencyMHz: frequencyMHz,
);
}
static double _kFactorForFrequency(double? frequencyMHz) {
if (frequencyMHz == null) return _baselineKFactor;
final delta =
(frequencyMHz - _baselineFrequencyMHz) / _baselineFrequencyMHz;
final adjustment = delta * 0.15;
final scaled = _baselineKFactor * (1 + adjustment);
return scaled.clamp(1.1, 1.6).toDouble();
}
List<LatLng> _buildSamplePoints(
LatLng start,
LatLng end,
double distanceMeters,
) {
final sampleCount = distanceMeters < 2000
? 21
: distanceMeters < 10000
? 41
: 81;
final points = <LatLng>[];
for (int i = 0; i < sampleCount; i++) {
final t = i / (sampleCount - 1);
points.add(
LatLng(
start.latitude + (end.latitude - start.latitude) * t,
start.longitude + (end.longitude - start.longitude) * t,
),
);
}
return points;
}
Future<List<double?>> _getElevations(List<LatLng> points) async {
final dataSource = _elevationDataSource;
if (dataSource != null) {
return dataSource(points);
}
final uncached = <int, LatLng>{};
final values = List<double?>.filled(points.length, null);
for (int i = 0; i < points.length; i++) {
final key = _cacheKey(points[i]);
final cached = _readCachedValue(key);
if (cached != null) {
values[i] = cached;
} else {
uncached[i] = points[i];
}
}
if (uncached.isEmpty) return values;
final latCsv = uncached.values
.map((p) => p.latitude.toStringAsFixed(6))
.join(',');
final lonCsv = uncached.values
.map((p) => p.longitude.toStringAsFixed(6))
.join(',');
final uri = Uri.parse(
'https://api.open-meteo.com/v1/elevation?latitude=$latCsv&longitude=$lonCsv',
);
final response = await _getWithBackoff(uri);
if (response.statusCode != 200) {
return values;
}
final decoded = jsonDecode(response.body);
if (decoded is! Map<String, dynamic>) {
return values;
}
final elevations = decoded['elevation'];
if (elevations is! List) {
return values;
}
final indices = uncached.keys.toList();
for (int i = 0; i < min(indices.length, elevations.length); i++) {
final value = elevations[i];
if (value is! num) continue;
final index = indices[i];
final elevation = value.toDouble();
values[index] = elevation;
_elevationCache[_cacheKey(points[index])] = _CachedElevation(
value: elevation,
expiresAt: DateTime.now().add(_cacheTtl),
);
}
return values;
}
Future<http.Response> _getWithBackoff(Uri uri) async {
var attempt = 0;
Duration backoff = _initialBackoff;
while (true) {
attempt++;
try {
final response = await _httpClient.get(uri);
if (!_shouldRetryStatus(response.statusCode) ||
attempt >= _maxFetchAttempts) {
return response;
}
} catch (_) {
if (attempt >= _maxFetchAttempts) rethrow;
}
await Future.delayed(backoff);
backoff *= 2;
}
}
bool _shouldRetryStatus(int statusCode) {
return statusCode == 429 || statusCode >= 500;
}
double? _readCachedValue(String key) {
final cached = _elevationCache[key];
if (cached == null) return null;
if (DateTime.now().isAfter(cached.expiresAt)) {
_elevationCache.remove(key);
return null;
}
return cached.value;
}
String _cacheKey(LatLng point) {
return '${point.latitude.toStringAsFixed(5)},${point.longitude.toStringAsFixed(5)}';
}
void dispose() {
if (_ownsHttpClient) {
_httpClient.close();
}
}
}
class _CachedElevation {
final double value;
final DateTime expiresAt;
const _CachedElevation({required this.value, required this.expiresAt});
}
+58 -189
View File
@@ -44,12 +44,6 @@ class MessageRetryService extends ChangeNotifier {
[]; // Rolling buffer of recent ACK hashes
final Map<String, List<String>> _pendingMessageQueuePerContact =
{}; // contactPubKeyHex FIFO queue of messageIds (DEPRECATED - will be removed)
final Map<String, List<String>> _sendQueue =
{}; // contactPubKeyHex ordered list of messageIds awaiting send
final Set<String> _activeMessages =
{}; // messageIds currently in-flight (sent/retrying)
final Set<String> _resolvedMessages =
{}; // messageIds already resolved (prevents double _onMessageResolved)
final Map<String, String> _expectedHashToMessageId =
{}; // expectedAckHashHex messageId (for matching RESP_CODE_SENT by hash)
@@ -58,13 +52,12 @@ class MessageRetryService extends ChangeNotifier {
Function(Message)? _updateMessageCallback;
Function(Contact)? _clearContactPathCallback;
Function(Contact, Uint8List, int)? _setContactPathCallback;
Function(int, int, {String? contactKey})? _calculateTimeoutCallback;
Function(int, int)? _calculateTimeoutCallback;
Uint8List? Function()? _getSelfPublicKeyCallback;
String Function(Contact, String)? _prepareContactOutboundTextCallback;
AppSettingsService? _appSettingsService;
AppDebugLogService? _debugLogService;
Function(String, PathSelection, bool, int?)? _recordPathResultCallback;
Function(String, int, int, int)? _onDeliveryObservedCallback;
MessageRetryService();
@@ -74,20 +67,12 @@ class MessageRetryService extends ChangeNotifier {
required Function(Message) updateMessageCallback,
Function(Contact)? clearContactPathCallback,
Function(Contact, Uint8List, int)? setContactPathCallback,
Function(int pathLength, int messageBytes, {String? contactKey})?
calculateTimeoutCallback,
Function(int pathLength, int messageBytes)? calculateTimeoutCallback,
Uint8List? Function()? getSelfPublicKeyCallback,
String Function(Contact, String)? prepareContactOutboundTextCallback,
AppSettingsService? appSettingsService,
AppDebugLogService? debugLogService,
Function(String, PathSelection, bool, int?)? recordPathResultCallback,
Function(
String contactKey,
int pathLength,
int messageBytes,
int tripTimeMs,
)?
onDeliveryObservedCallback,
}) {
_sendMessageCallback = sendMessageCallback;
_addMessageCallback = addMessageCallback;
@@ -100,7 +85,6 @@ class MessageRetryService extends ChangeNotifier {
_appSettingsService = appSettingsService;
_debugLogService = debugLogService;
_recordPathResultCallback = recordPathResultCallback;
_onDeliveryObservedCallback = onDeliveryObservedCallback;
}
/// Compute expected ACK hash using same algorithm as firmware:
@@ -172,49 +156,7 @@ class MessageRetryService extends ChangeNotifier {
_addMessageCallback!(contact.publicKeyHex, message);
}
// Queue per contact only one message in-flight at a time to avoid
// overflowing the firmware's 8-entry expected_ack_table.
final contactKey = contact.publicKeyHex;
_sendQueue[contactKey] ??= [];
_sendQueue[contactKey]!.add(messageId);
if (!_activeMessages.any(
(id) => _pendingContacts[id]?.publicKeyHex == contactKey,
)) {
_sendNextForContact(contactKey);
}
}
void _sendNextForContact(String contactKey) {
final queue = _sendQueue[contactKey];
if (queue == null) return;
// Drain stale entries iteratively instead of recursing.
while (queue.isNotEmpty) {
final messageId = queue.removeAt(0);
if (_pendingMessages.containsKey(messageId)) {
_activeMessages.add(messageId);
_attemptSend(messageId).catchError((e) {
debugPrint('_attemptSend threw for $messageId: $e');
final msg = _pendingMessages[messageId];
if (msg != null) {
final failed = msg.copyWith(status: MessageStatus.failed);
_pendingMessages[messageId] = failed;
_updateMessageCallback?.call(failed);
}
_onMessageResolved(messageId, contactKey);
});
return;
}
// Message was cancelled/cleaned up while queued try next
}
}
void _onMessageResolved(String messageId, String contactKey) {
if (_resolvedMessages.contains(messageId)) return;
_resolvedMessages.add(messageId);
_activeMessages.remove(messageId);
_sendNextForContact(contactKey);
await _attemptSend(messageId);
}
Future<void> _attemptSend(String messageId) async {
@@ -227,11 +169,13 @@ class MessageRetryService extends ChangeNotifier {
// Use the path that was captured when the message was first sent
if (_setContactPathCallback != null && _clearContactPathCallback != null) {
if (message.pathLength != null && message.pathLength! < 0) {
// Flood mode - clear the path
debugPrint(
'Setting flood mode for retry attempt ${message.retryCount}',
);
await _clearContactPathCallback!(contact);
_clearContactPathCallback!(contact);
} else if (message.pathLength != null && message.pathLength! >= 0) {
// Specific path (including direct neighbor with pathLength=0)
final pathStr = message.pathBytes.isEmpty
? 'direct'
: message.pathBytes
@@ -248,24 +192,6 @@ class MessageRetryService extends ChangeNotifier {
}
}
// Re-validate after async gap a timer or ACK could have resolved/retried
// this message while we were awaiting the path callback.
final currentMessage = _pendingMessages[messageId];
if (currentMessage == null || _resolvedMessages.contains(messageId)) {
debugPrint(
'_attemptSend: message $messageId resolved during path sync, aborting',
);
return;
}
// If the message was retried by a timer during our await, the retryCount
// will have advanced. Only proceed if it still matches the attempt we started.
if (currentMessage.retryCount != message.retryCount) {
debugPrint(
'_attemptSend: message $messageId retryCount changed during path sync, aborting',
);
return;
}
final attempt = message.retryCount.clamp(0, 3);
final timestampSeconds = message.timestamp.millisecondsSinceEpoch ~/ 1000;
@@ -305,23 +231,10 @@ class MessageRetryService extends ChangeNotifier {
if (_sendMessageCallback != null) {
_sendMessageCallback!(contact, message.text, attempt, timestampSeconds);
} else {
// No send callback message would be stuck forever. Fail it immediately.
debugPrint(
'_attemptSend: no sendMessageCallback, failing message $messageId',
);
final failedMessage = message.copyWith(status: MessageStatus.failed);
_pendingMessages[messageId] = failedMessage;
_updateMessageCallback?.call(failedMessage);
_onMessageResolved(messageId, contact.publicKeyHex);
}
}
bool updateMessageFromSent(
Uint8List ackHash,
int timeoutMs, {
bool allowQueueFallback = true,
}) {
void updateMessageFromSent(Uint8List ackHash, int timeoutMs) {
final ackHashHex = ackHash
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join();
@@ -364,8 +277,7 @@ class MessageRetryService extends ChangeNotifier {
}
// FALLBACK: Old queue-based matching (for messages sent before hash computation was added)
// Only match within a single contact's queue to avoid cross-contact mismatches.
if (messageId == null && allowQueueFallback) {
if (messageId == null) {
_debugLogService?.warn(
'RESP_CODE_SENT: ACK hash $ackHashHex not found in hash table, falling back to queue',
tag: 'AckHash',
@@ -374,16 +286,13 @@ class MessageRetryService extends ChangeNotifier {
'Hash-based match failed for $ackHashHex, falling back to queue-based matching',
);
// Search all contact queues so concurrent chats don't miss matches.
final queuesToSearch = _pendingMessageQueuePerContact;
for (var entry in queuesToSearch.entries) {
for (var entry in _pendingMessageQueuePerContact.entries) {
final contactKey = entry.key;
final queue = entry.value;
// Drain stale entries until we find a valid one or exhaust the queue.
while (queue.isNotEmpty) {
if (queue.isNotEmpty) {
final candidateMessageId = queue.removeAt(0);
if (_pendingMessages.containsKey(candidateMessageId)) {
messageId = candidateMessageId;
contact = _pendingContacts[candidateMessageId];
@@ -391,16 +300,27 @@ class MessageRetryService extends ChangeNotifier {
'Queue-based match (fallback): $ackHashHex → message $messageId for $contactKey',
);
break;
} else {
debugPrint('Dequeued stale message $candidateMessageId - skipping');
if (queue.isNotEmpty) {
final nextMessageId = queue.removeAt(0);
if (_pendingMessages.containsKey(nextMessageId)) {
messageId = nextMessageId;
contact = _pendingContacts[nextMessageId];
debugPrint(
'Queue-based match (fallback): $ackHashHex → message $messageId',
);
break;
}
}
}
debugPrint('Dequeued stale message $candidateMessageId - skipping');
}
if (messageId != null) break;
}
}
if (messageId == null || contact == null) {
debugPrint('No pending message found for ACK hash: $ackHashHex');
return false;
return;
}
// Store the mapping for future lookups (e.g., when ACK arrives)
@@ -419,7 +339,7 @@ class MessageRetryService extends ChangeNotifier {
'Message $messageId no longer pending for ACK hash: $ackHashHex',
);
_ackHashToMessageId.remove(ackHashHex);
return false;
return;
}
// Add this ACK hash to the list of expected ACKs for this message (for history)
@@ -433,33 +353,25 @@ class MessageRetryService extends ChangeNotifier {
);
}
// Calculate timeout: prefer ML prediction, then device-provided, then physics fallback
int pathLengthValue;
if (selection != null) {
pathLengthValue = selection.useFlood ? -1 : selection.hopCount;
if (pathLengthValue < 0) pathLengthValue = contact.pathLength;
} else if (message.pathLength != null) {
pathLengthValue = message.pathLength!;
} else {
pathLengthValue = contact.pathLength;
}
// Use device-provided timeout, or calculate from radio settings if timeout is 0 or invalid
int actualTimeout = timeoutMs;
if (_calculateTimeoutCallback != null) {
final calculated = _calculateTimeoutCallback!(
if (timeoutMs <= 0 && _calculateTimeoutCallback != null) {
int pathLengthValue;
if (selection != null) {
pathLengthValue = selection.useFlood ? -1 : selection.hopCount;
if (pathLengthValue < 0) pathLengthValue = contact.pathLength;
} else if (message.pathLength != null) {
pathLengthValue = message.pathLength!;
} else {
pathLengthValue = contact.pathLength;
}
actualTimeout = _calculateTimeoutCallback!(
pathLengthValue,
message.text.length,
contactKey: contact.publicKeyHex,
);
// calculateTimeout tries ML first, falls back to physics.
// Use calculated value if device didn't provide one, or if ML
// produced a tighter prediction than the device's estimate.
if (timeoutMs <= 0 || calculated < timeoutMs) {
actualTimeout = calculated;
debugPrint(
'Using calculated timeout: ${actualTimeout}ms for path length $pathLengthValue',
);
}
debugPrint(
'Using calculated timeout: ${actualTimeout}ms for path length $pathLengthValue',
);
}
final updatedMessage = message.copyWith(
@@ -477,11 +389,8 @@ class MessageRetryService extends ChangeNotifier {
_startTimeoutTimer(messageId, actualTimeout);
debugPrint('Updated message $messageId with ACK hash: $ackHashHex');
return true;
}
bool get hasPendingMessages => _pendingMessages.isNotEmpty;
void _startTimeoutTimer(String messageId, int timeoutMs) {
_timeoutTimers[messageId]?.cancel();
_timeoutTimers[messageId] = Timer(Duration(milliseconds: timeoutMs), () {
@@ -547,7 +456,22 @@ class MessageRetryService extends ChangeNotifier {
} else {
// Max retries reached - mark as failed
final failedMessage = message.copyWith(status: MessageStatus.failed);
_pendingMessages[messageId] = failedMessage;
// Move ACK hashes to history before removing
_moveAckHashesToHistory(messageId);
_pendingMessages.remove(messageId);
_pendingContacts.remove(messageId);
_pendingPathSelections.remove(messageId);
_timeoutTimers[messageId]?.cancel();
_timeoutTimers.remove(messageId);
// Clean up the queue entry for this contact
_pendingMessageQueuePerContact[contact.publicKeyHex]?.remove(messageId);
if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ??
false) {
_pendingMessageQueuePerContact.remove(contact.publicKeyHex);
}
// Check if we should clear the path on max retry
if (_appSettingsService?.settings.clearPathOnMaxRetry == true &&
@@ -568,30 +492,6 @@ class MessageRetryService extends ChangeNotifier {
}
notifyListeners();
// Message is done retrying send next queued message for this contact
_onMessageResolved(messageId, contact.publicKeyHex);
// Keep message in pending maps for 30s grace period so late ACKs
// can still match and update the message to delivered.
_timeoutTimers[messageId] = Timer(const Duration(seconds: 30), () {
_moveAckHashesToHistory(messageId);
// Clean up ALL hash mappings for this message
_ackHashToMessageId.removeWhere(
(_, mapping) => mapping.messageId == messageId,
);
_expectedHashToMessageId.removeWhere((_, msgId) => msgId == messageId);
_pendingMessages.remove(messageId);
_pendingContacts.remove(messageId);
_pendingPathSelections.remove(messageId);
_timeoutTimers.remove(messageId);
_resolvedMessages.remove(messageId);
final contactKey = contact.publicKeyHex;
_pendingMessageQueuePerContact[contactKey]?.remove(messageId);
if (_pendingMessageQueuePerContact[contactKey]?.isEmpty ?? false) {
_pendingMessageQueuePerContact.remove(contactKey);
}
});
}
}
@@ -687,15 +587,7 @@ class MessageRetryService extends ChangeNotifier {
}
if (matchedMessageId != null) {
final message = _pendingMessages[matchedMessageId];
if (message == null) {
// Message was already cleaned up (e.g. grace period expired)
_ackHashToMessageId.remove(ackHashHex);
debugPrint(
'ACK matched $matchedMessageId but message already cleaned up',
);
return;
}
final message = _pendingMessages[matchedMessageId]!;
final contact = _pendingContacts[matchedMessageId];
final selection = _pendingPathSelections[matchedMessageId];
@@ -717,21 +609,12 @@ class MessageRetryService extends ChangeNotifier {
tripTimeMs: tripTimeMs,
);
// Clean up ALL hash mappings for this message (from all retry attempts)
_ackHashToMessageId.removeWhere(
(_, mapping) => mapping.messageId == matchedMessageId,
);
_expectedHashToMessageId.removeWhere(
(_, msgId) => msgId == matchedMessageId,
);
// Move ACK hashes to history before removing
_moveAckHashesToHistory(matchedMessageId);
_pendingMessages.remove(matchedMessageId);
_pendingContacts.remove(matchedMessageId);
_pendingPathSelections.remove(matchedMessageId);
_resolvedMessages.remove(matchedMessageId);
// Clean up the queue entry for this contact (remove any remaining references to this message)
if (contact != null) {
@@ -756,17 +639,6 @@ class MessageRetryService extends ChangeNotifier {
true,
tripTimeMs,
);
if (_onDeliveryObservedCallback != null &&
tripTimeMs > 0 &&
message.pathLength != null) {
_onDeliveryObservedCallback!(
contact.publicKeyHex,
message.pathLength!,
message.text.length,
tripTimeMs,
);
}
_onMessageResolved(matchedMessageId, contact.publicKeyHex);
}
notifyListeners();
@@ -904,9 +776,6 @@ class MessageRetryService extends ChangeNotifier {
_ackHistory.clear();
_ackHashToMessageId.clear();
_pendingMessageQueuePerContact.clear();
_sendQueue.clear();
_activeMessages.clear();
_resolvedMessages.clear();
super.dispose();
}
}
+64 -172
View File
@@ -1,11 +1,9 @@
import 'dart:io' show Platform, File;
import 'dart:ui';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter/foundation.dart';
import '../l10n/app_localizations.dart';
import '../utils/platform_info.dart';
class NotificationService {
static final NotificationService _instance = NotificationService._internal();
@@ -60,35 +58,16 @@ class NotificationService {
requestBadgePermission: true,
requestSoundPermission: true,
);
const windowsSettings = WindowsInitializationSettings(
appName: 'MeshCore Open',
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,
initSettings,
onDidReceiveNotificationResponse: _onNotificationTapped,
);
_isInitialized = true;
@@ -97,22 +76,6 @@ 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();
}
return _isInitialized;
}
Future<bool> requestPermissions() async {
if (!_isInitialized) {
await initialize();
@@ -151,7 +114,9 @@ class NotificationService {
String? contactId,
int? badgeCount,
}) async {
if (!await _ensureInitialized()) return;
if (!_isInitialized) {
await initialize();
}
final androidDetails = AndroidNotificationDetails(
'messages',
@@ -183,17 +148,13 @@ class NotificationService {
macOS: macDetails,
);
try {
await _notifications.show(
id: contactId?.hashCode ?? 0,
title: contactName,
body: message,
notificationDetails: notificationDetails,
payload: 'message:$contactId',
);
} catch (e) {
debugPrint('Failed to show message notification: $e');
}
await _notifications.show(
contactId?.hashCode ?? 0,
contactName,
message,
notificationDetails,
payload: 'message:$contactId',
);
}
Future<void> _showAdvertNotificationImpl({
@@ -201,7 +162,9 @@ class NotificationService {
required String contactType,
String? contactId,
}) async {
if (!await _ensureInitialized()) return;
if (!_isInitialized) {
await initialize();
}
const androidDetails = AndroidNotificationDetails(
'adverts',
@@ -230,19 +193,13 @@ class NotificationService {
macOS: macDetails,
);
try {
await _notifications.show(
id: contactId != null
? 'advert:$contactId'.hashCode
: DateTime.now().millisecondsSinceEpoch,
title: _l10n.notification_newTypeDiscovered(contactType),
body: contactName,
notificationDetails: notificationDetails,
payload: 'advert:$contactId',
);
} catch (e) {
debugPrint('Failed to show advert notification: $e');
}
await _notifications.show(
contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
_l10n.notification_newTypeDiscovered(contactType),
contactName,
notificationDetails,
payload: 'advert:$contactId',
);
}
Future<void> _showChannelMessageNotificationImpl({
@@ -251,7 +208,9 @@ class NotificationService {
int? channelIndex,
int? badgeCount,
}) async {
if (!await _ensureInitialized()) return;
if (!_isInitialized) {
await initialize();
}
final androidDetails = AndroidNotificationDetails(
'channel_messages',
@@ -288,17 +247,13 @@ class NotificationService {
? _l10n.notification_receivedNewMessage
: preview;
try {
await _notifications.show(
id: channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
title: channelName,
body: body,
notificationDetails: notificationDetails,
payload: 'channel:$channelIndex',
);
} catch (e) {
debugPrint('Failed to show channel notification: $e');
}
await _notifications.show(
channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
channelName,
body,
notificationDetails,
payload: 'channel:$channelIndex',
);
}
/// Returns a privacy-safe identifier for debug logging.
@@ -330,62 +285,7 @@ class NotificationService {
}
Future<void> cancel(int id) async {
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.
await _notifications.cancel(id);
}
//
@@ -496,39 +396,35 @@ class NotificationService {
Future<void> _showNotificationImmediately(
_PendingNotification notification,
) async {
try {
switch (notification.type) {
case _NotificationType.message:
await _showMessageNotificationImpl(
contactName: notification.title,
message: notification.body,
contactId: notification.id,
badgeCount: notification.badgeCount,
);
break;
case _NotificationType.advert:
await _showAdvertNotificationImpl(
contactName: notification.body,
contactType: notification.title,
contactId: notification.id,
);
break;
case _NotificationType.channelMessage:
await _showChannelMessageNotificationImpl(
channelName: notification.title,
message: notification.body,
channelIndex: int.tryParse(notification.id ?? ''),
badgeCount: notification.badgeCount,
);
break;
}
} catch (e) {
debugPrint('Failed to show immediate notification: $e');
switch (notification.type) {
case _NotificationType.message:
await _showMessageNotificationImpl(
contactName: notification.title,
message: notification.body,
contactId: notification.id,
badgeCount: notification.badgeCount,
);
break;
case _NotificationType.advert:
await _showAdvertNotificationImpl(
contactName: notification.body,
contactType: notification.title,
contactId: notification.id,
);
break;
case _NotificationType.channelMessage:
await _showChannelMessageNotificationImpl(
channelName: notification.title,
message: notification.body,
channelIndex: int.tryParse(notification.id ?? ''),
badgeCount: notification.badgeCount,
);
break;
}
}
Future<void> _showBatchSummary(List<_PendingNotification> batch) async {
if (!await _ensureInitialized()) return;
if (!_isInitialized) await initialize();
// Group by type
final messages = batch
@@ -572,17 +468,13 @@ class NotificationService {
const notificationDetails = NotificationDetails(android: androidDetails);
try {
await _notifications.show(
id: 'batch_summary'.hashCode,
title: _l10n.notification_activityTitle,
body: parts.join(', '),
notificationDetails: notificationDetails,
payload: 'batch',
);
} catch (e) {
debugPrint('Failed to show batch summary notification: $e');
}
await _notifications.show(
'batch_summary'.hashCode,
_l10n.notification_activityTitle,
parts.join(', '),
notificationDetails,
payload: 'batch',
);
}
}
-7
View File
@@ -15,9 +15,6 @@ class PathHistoryService extends ChangeNotifier {
final List<String> _cacheAccessOrder = [];
static const int _maxHistoryEntries = 100;
int _version = 0;
int get version => _version;
static const int _autoRotationTopCount = 3;
PathHistoryService(this._storage);
@@ -188,7 +185,6 @@ class PathHistoryService extends ChangeNotifier {
) {
var history = _cache[contactPubKeyHex];
if (history == null) return;
_version++;
final existing = _findPathRecord(contactPubKeyHex, pathBytes);
if (existing != null) {
@@ -245,7 +241,6 @@ class PathHistoryService extends ChangeNotifier {
_cache[contactPubKeyHex] = loaded;
_trackAccess(contactPubKeyHex);
_evictIfNeeded();
_version++;
notifyListeners();
}
});
@@ -281,7 +276,6 @@ class PathHistoryService extends ChangeNotifier {
_autoRotationIndex.remove(contactPubKeyHex);
_floodStats.remove(contactPubKeyHex);
await _storage.clearPathHistory(contactPubKeyHex);
_version++;
notifyListeners();
}
@@ -301,7 +295,6 @@ class PathHistoryService extends ChangeNotifier {
);
await _storage.savePathHistory(contactPubKeyHex, _cache[contactPubKeyHex]!);
_version++;
notifyListeners();
}
+244
View File
@@ -0,0 +1,244 @@
import 'dart:async';
import 'dart:io';
import 'dart:math' as math;
import 'package:flutter/widgets.dart';
import 'package:geolocator/geolocator.dart';
import 'package:gpx/gpx.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:share_plus/share_plus.dart';
class SparseLocationLogger {
static const double distanceThresholdMiles = 0.25;
static const double distanceThresholdMeters =
distanceThresholdMiles * 1609.34;
static const double headingChangeThresholdDeg = 35.0;
static const double minSpeedForTurnKmh = 8.0;
static const double minTime = 120.0; // seconds
Position? _lastLoggedPosition;
double? _lastHeading;
DateTime? _lastLoggedTime;
StreamSubscription<Position>? _positionStream;
Timer? _timer;
Function(Position position)? _onNewLogPoint;
// GPX structures
final Gpx _gpx = Gpx();
Trkseg _currentSegment = Trkseg(); // one segment for the whole session
File? _gpxFile;
bool _isInitialized = false;
void initialize(Function(Position position) onNewLogPoint) {
_onNewLogPoint = onNewLogPoint;
}
Future<bool> getPremissons() async {
// Permissions & service check (same as before)
var status = await Permission.location.request();
if (!status.isGranted) {
debugPrint('Location permission denied');
return false;
}
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
debugPrint('Location services disabled');
return false;
}
return true;
}
Future<void> startLogging() async {
if (!await getPremissons()) {
return;
}
// Prepare files
final directory = await getApplicationDocumentsDirectory();
final timestamp = DateTime.now()
.toIso8601String()
.replaceAll(':', '-')
.split('.')
.first;
_gpxFile = File('${directory.path}/track_$timestamp.gpx');
// Init GPX metadata
_gpx.metadata = Metadata(
name: 'Sparse Track ${DateTime.now().toString().split(' ').first}',
desc: 'Sparse GPS log: ~every 1.5 mi or significant turns',
time: DateTime.now(),
);
// Add one track with one segment
final track = Trk(name: 'Main Track');
_currentSegment = Trkseg();
track.trksegs.add(_currentSegment);
_gpx.trks.add(track);
_isInitialized = true;
// Start location stream
_positionStream = Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 152, // meters (~0.16 mi) - helps battery
),
).listen(_onPositionReceived);
// Also poll via timer as fallback
_timer = Timer.periodic(Duration(seconds: (minTime / 2).toInt()), (
_,
) async {
final position = await Geolocator.getCurrentPosition();
await _onPositionReceived(position);
});
_lastLoggedPosition = null;
_lastHeading = null;
_lastLoggedTime = null;
debugPrint('Sparse GPX logging started → ${_gpxFile?.path}');
}
Future<void> stopLogging() async {
await _positionStream?.cancel();
_positionStream = null;
_timer?.cancel();
_timer = null;
if (_isInitialized && _currentSegment.trkpts.isNotEmpty) {
// Write GPX file on stop
final xmlString = GpxWriter().asString(_gpx, pretty: true);
await _gpxFile?.writeAsString(xmlString);
await SharePlus.instance.share(
ShareParams(
text: 'Sparse GPS track',
subject: 'Sparse GPS track',
files: [XFile(_gpxFile?.path ?? '')],
),
);
await _gpxFile?.delete();
}
debugPrint('Logging stopped');
}
Future<void> updateMyLocation() async {
if (!await getPremissons()) {
return;
}
try {
final position = await Geolocator.getCurrentPosition();
_onNewLogPoint?.call(position);
} catch (e) {
debugPrint('Error updating location: $e');
}
}
Future<void> _onPositionReceived(Position position) async {
final now = DateTime.now();
final speedKmh = position.speed * 3.6;
final heading = position.heading;
bool shouldLog = false;
String reason = '';
if (_lastLoggedPosition == null) {
shouldLog = true;
reason = 'start';
} else {
final distanceMeters = Geolocator.distanceBetween(
_lastLoggedPosition!.latitude,
_lastLoggedPosition!.longitude,
position.latitude,
position.longitude,
);
if (distanceMeters >= distanceThresholdMeters) {
shouldLog = true;
reason =
'distance (${(distanceMeters / 1609.34).toStringAsFixed(2)} mi)';
} else if (speedKmh > minSpeedForTurnKmh && _lastHeading != null) {
double delta = (heading - _lastHeading!).abs();
delta = math.min(delta, 360 - delta);
if (delta > headingChangeThresholdDeg) {
shouldLog = true;
reason = 'turn (${delta.toStringAsFixed(1)}°)';
}
} else if (_lastLoggedTime != null) {
final elapsed = now.difference(_lastLoggedTime!).inSeconds;
if (elapsed >= minTime && distanceMeters >= distanceThresholdMeters) {
shouldLog = true;
reason = 'time (${elapsed}s)';
}
}
}
if (shouldLog) {
// Create GPX Waypoint (trkpt)
final pt = Wpt(
lat: position.latitude,
lon: position.longitude,
ele: position.altitude, // if available
time: now,
extensions: {
"course": ?heading.isFinite ? heading : null,
"speed": ?speedKmh > 0 ? speedKmh / 3.6 : null, // GPX speed in m/s
},
// You can add hdop, vdop, etc. from position if desired
);
_currentSegment.trkpts.add(pt);
_onNewLogPoint?.call(position);
debugPrint('Logged point: ${pt.lat}, ${pt.lon} ($reason)');
_lastLoggedPosition = position;
_lastHeading = heading;
_lastLoggedTime = now;
} else {
debugPrint('Skipped point: ${position.latitude}, ${position.longitude}');
}
}
Position snapToGridCenter({
required Position position,
required double cellSizeMeters,
}) {
Position snappedPosition = position;
// Snap latitude
final latFloor =
(position.latitude / cellSizeMeters).floor() * cellSizeMeters;
final snappedLat = latFloor + (cellSizeMeters / 2);
// Snap longitude
final lonFloor =
(position.longitude / cellSizeMeters).floor() * cellSizeMeters;
final snappedLon = lonFloor + (cellSizeMeters / 2);
snappedPosition = Position(
latitude: snappedLat,
longitude: snappedLon,
altitude: position.altitude,
accuracy: position.accuracy,
heading: position.heading,
speed: position.speed,
speedAccuracy: position.speedAccuracy,
altitudeAccuracy: position.altitudeAccuracy,
headingAccuracy: position.headingAccuracy,
timestamp: position.timestamp,
);
return snappedPosition;
}
Future<String> getGpxFilePath() async => _gpxFile?.path ?? 'Not started';
bool isLogging() => _positionStream != null;
int getPointCount() => _currentSegment.trkpts.length;
}
-31
View File
@@ -1,5 +1,4 @@
import 'dart:convert';
import '../models/delivery_observation.dart';
import '../models/path_history.dart';
import '../storage/prefs_manager.dart';
@@ -7,7 +6,6 @@ class StorageService {
static const String _pathHistoryPrefix = 'path_history_';
static const String _pendingMessagesKey = 'pending_messages';
static const String _repeaterPasswordsKey = 'repeater_passwords';
static const String _deliveryObservationsKey = 'delivery_observations';
Future<void> savePathHistory(
String contactPubKeyHex,
@@ -124,33 +122,4 @@ 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);
}
}
-2
View File
@@ -1,2 +0,0 @@
export 'tcp_transport_service_native.dart'
if (dart.library.js_interop) 'tcp_transport_service_web.dart';
@@ -1,210 +0,0 @@
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 }
@@ -1,35 +0,0 @@
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() {}
}
@@ -1,229 +0,0 @@
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());
}
}
}
-111
View File
@@ -1,111 +0,0 @@
import 'dart:typed_data';
const int usbSerialTxFrameStart = 0x3c;
const int usbSerialRxFrameStart = 0x3e;
const int usbSerialHeaderLength = 3;
const int usbSerialMaxPayloadLength = 172;
Uint8List wrapUsbSerialTxFrame(Uint8List payload) {
if (payload.length > usbSerialMaxPayloadLength) {
throw ArgumentError.value(
payload.length,
'payload.length',
'USB serial payload exceeds $usbSerialMaxPayloadLength bytes',
);
}
final packet = Uint8List(usbSerialHeaderLength + payload.length);
packet[0] = usbSerialTxFrameStart;
packet[1] = payload.length & 0xff;
packet[2] = (payload.length >> 8) & 0xff;
packet.setRange(usbSerialHeaderLength, packet.length, payload);
return packet;
}
class UsbSerialDecodedPacket {
const UsbSerialDecodedPacket({
required this.frameStart,
required this.payload,
});
final int frameStart;
final Uint8List payload;
bool get isRxFrame => frameStart == usbSerialRxFrameStart;
}
class UsbSerialFrameDecoder {
final List<int> _rxBuffer = <int>[];
int _startIndex = 0;
void reset() {
_rxBuffer.clear();
_startIndex = 0;
}
List<UsbSerialDecodedPacket> ingest(Uint8List bytes) {
if (bytes.isEmpty) {
return const <UsbSerialDecodedPacket>[];
}
_rxBuffer.addAll(bytes);
final packets = <UsbSerialDecodedPacket>[];
while (true) {
if (_startIndex >= _rxBuffer.length) {
_rxBuffer.clear();
_startIndex = 0;
return packets;
}
if (_rxBuffer[_startIndex] != usbSerialRxFrameStart &&
_rxBuffer[_startIndex] != usbSerialTxFrameStart) {
_startIndex++;
_compactBufferIfNeeded();
continue;
}
final availableLength = _rxBuffer.length - _startIndex;
if (availableLength < usbSerialHeaderLength) {
_compactBufferIfNeeded(force: true);
return packets;
}
final payloadLength =
_rxBuffer[_startIndex + 1] | (_rxBuffer[_startIndex + 2] << 8);
if (payloadLength > usbSerialMaxPayloadLength) {
_startIndex++;
_compactBufferIfNeeded();
continue;
}
final packetLength = usbSerialHeaderLength + payloadLength;
if (availableLength < packetLength) {
_compactBufferIfNeeded(force: true);
return packets;
}
final frameStart = _rxBuffer[_startIndex];
final payload = Uint8List.fromList(
_rxBuffer.sublist(
_startIndex + usbSerialHeaderLength,
_startIndex + packetLength,
),
);
_startIndex += packetLength;
_compactBufferIfNeeded();
packets.add(
UsbSerialDecodedPacket(frameStart: frameStart, payload: payload),
);
}
}
void _compactBufferIfNeeded({bool force = false}) {
if (_startIndex == 0) {
return;
}
if (!force && _startIndex < 1024 && _startIndex < (_rxBuffer.length ~/ 2)) {
return;
}
_rxBuffer.removeRange(0, _startIndex);
_startIndex = 0;
}
}
-2
View File
@@ -1,2 +0,0 @@
export 'usb_serial_service_native.dart'
if (dart.library.js_interop) 'usb_serial_service_web.dart';
-463
View File
@@ -1,463 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:flserial/flserial.dart';
import 'package:flserial/flserial_exception.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'app_debug_log_service.dart';
import '../utils/macos_usb_device_names.dart';
import '../utils/platform_info.dart';
import '../utils/usb_port_labels.dart';
import 'usb_serial_frame_codec.dart';
/// Wraps the native flserial plugin to expose a stream of raw bytes for the
/// MeshCore connector to consume.
class UsbSerialService {
UsbSerialService();
static const MethodChannel _androidMethodChannel = MethodChannel(
'meshcore_open/android_usb_serial',
);
static const EventChannel _androidEventChannel = EventChannel(
'meshcore_open/android_usb_serial_events',
);
final StreamController<Uint8List> _frameController =
StreamController<Uint8List>.broadcast();
final UsbSerialFrameDecoder _frameDecoder = UsbSerialFrameDecoder();
StreamSubscription<dynamic>? _androidDataSubscription;
StreamSubscription<FlSerialEventArgs>? _dataSubscription;
UsbSerialStatus _status = UsbSerialStatus.disconnected;
String? _connectedPortKey;
String? _connectedPortLabel;
FlSerial? _serial;
AppDebugLogService? _debugLogService;
UsbSerialStatus get status => _status;
String? get activePortKey => _connectedPortKey;
String? get activePortDisplayLabel =>
_connectedPortLabel ?? _connectedPortKey;
Stream<Uint8List> get frameStream => _frameController.stream;
bool get _useAndroidUsbHost =>
!kIsWeb && defaultTargetPlatform == TargetPlatform.android;
bool get _useDesktopFlSerial =>
PlatformInfo.isWindows || PlatformInfo.isLinux || PlatformInfo.isMacOS;
bool get _isSupportedPlatform => _useAndroidUsbHost || _useDesktopFlSerial;
// Always-fresh: do NOT use ??= here a cached FlSerial retains stale
// native handle state (flh) from a prior failed open, causing subsequent
// open attempts to fail with "port not exist" even when the device is present.
FlSerial _freshSerial() => FlSerial();
bool get isConnected {
if (!_isSupportedPlatform) {
return false;
}
// Trust _status as the authoritative connection state. Polling
// _serial?.isOpen() via the native FL_CTRL_IS_PORT_OPEN query is
// unreliable during the brief USB re-enumeration window that many
// microcontrollers (e.g. NRF52) trigger in response to DTR assertion.
// Actual port drops are handled by the onDone / onError callbacks on the
// serial data stream subscription, which update _status correctly.
return _status == UsbSerialStatus.connected;
}
Future<List<String>> listPorts() async {
if (!_isSupportedPlatform) {
return const <String>[];
}
if (_useAndroidUsbHost) {
final ports = await _androidMethodChannel.invokeListMethod<String>(
'listPorts',
);
return ports ?? <String>[];
}
final rawPorts = FlSerial.listPorts();
// On macOS, flserial's native device-name lookup is broken on macOS
// 10.15+ because the IOKit class name changed from IOUSBDevice to
// IOUSBHostDevice. We resolve names ourselves via ioreg and rewrite any
// "port - n/a" entries with the real product name.
if (Platform.isMacOS && rawPorts.isNotEmpty) {
return _annotateMacOsPorts(rawPorts);
}
return Future.value(rawPorts);
}
/// Rewrites the flserial port list on macOS by substituting real USB device
/// names (obtained via [ioreg]) for the "n/a" placeholders that flserial
/// returns when it can't find the deprecated IOUSBDevice parent.
Future<List<String>> _annotateMacOsPorts(List<String> rawPorts) async {
final deviceNames = await queryMacOsUsbDeviceNames();
if (deviceNames.isEmpty) return rawPorts;
return rawPorts.map((entry) {
// entry format from fl_ports: "port - description - hardware_id"
final port = normalizeUsbPortName(entry); // e.g. /dev/cu.usbmodem1101
final knownName = deviceNames[port]; // e.g. "Nordic NRF52 DK"
if (knownName == null) return entry; // non-USB port, keep as-is
// Replace description field only; preserve hardware_id for device
// identity (used by normalizeUsbPortName).
final segments = entry.split(' - ');
final hardwareId = segments.length >= 3 ? segments.last : 'n/a';
return '$port - $knownName - $hardwareId';
}).toList();
}
void setDebugLogService(AppDebugLogService? service) {
_debugLogService = service;
}
Future<void> connect({
required String portName,
int baudRate = 115200,
}) async {
if (_status == UsbSerialStatus.connected ||
_status == UsbSerialStatus.connecting) {
throw StateError('USB serial transport is already active');
}
if (!_isSupportedPlatform) {
throw UnsupportedError('USB serial is not supported on this platform.');
}
_status = UsbSerialStatus.connecting;
var normalizedPortName = normalizeUsbPortName(portName);
_frameDecoder.reset();
if (_useAndroidUsbHost) {
try {
await _androidMethodChannel.invokeMethod<void>('connect', {
'portName': normalizedPortName,
'baudRate': baudRate,
});
_debugLogService?.info(
'USB serial opened port=$normalizedPortName on Android via USB host bridge',
tag: 'USB Serial',
);
} on PlatformException catch (error) {
_status = UsbSerialStatus.disconnected;
final msg = error.message ?? error.code;
_debugLogService?.error(
'Android connect failed: $msg',
tag: 'USB Serial',
);
rethrow;
}
} else {
// Hot-restart guard
// On hot restart Dart tears down the isolate without calling dispose().
// The NativeCallable registered by flserial's setCallback() is
// isolate-local and gets freed when the isolate dies, but the native
// SerialThread is still alive and will call it crash.
//
// flserial uses process-global native state. Calling fl_free() kills ALL
// SerialThreads for every open port across all Dart isolates (there is
// only one in a Flutter app). Then fl_init() re-initialises the slot
// table so subsequent fl_open() calls work normally.
//
// This must happen before we register any new NativeCallable, so it must
// be the very first thing we do in the desktop branch.
try {
bindings.fl_free();
bindings.fl_init(16);
} catch (_) {}
// On macOS, flserial lists both cu.* and tty.* device nodes.
// When a cu.* open fails with FL_ERROR_PORT_NOT_EXIST, try the tty.*
// variant as a fallback (and vice-versa) before giving up.
final candidates = _buildPortCandidates(normalizedPortName);
FlSerialException? lastError;
bool opened = false;
for (final candidate in candidates) {
// Always create a fresh FlSerial instance a cached instance retains
// a stale flh handle from prior failed opens, which causes the native
// fl_open() to mis-route the request and report port-not-exist even
// when the device node is physically present.
final serial = _freshSerial();
serial.init();
try {
final openStatus = serial.openPort(candidate, baudRate);
if (openStatus != FlOpenStatus.open) {
final msg =
'Failed to open USB port $candidate (status: $openStatus)';
_debugLogService?.error(msg, tag: 'USB Serial');
// Not a FlSerialException treat as terminal failure
_status = UsbSerialStatus.disconnected;
throw StateError(msg);
}
serial.setByteSize8();
serial.setBitParityNone();
serial.setStopBits1();
serial.setFlowControlNone();
serial.setRTS(false);
serial.setDTR(true);
_serial = serial;
// Update the normalized port name to whichever candidate succeeded.
normalizedPortName = candidate;
_debugLogService?.info(
'USB serial opened port=$candidate cts=${serial.getCTS()} dsr=${serial.getDSR()} dtr=true rts=false',
tag: 'USB Serial',
);
opened = true;
break;
} on FlSerialException catch (error) {
// The native fl_open() already called fl_close() on failure
// internally, so no extra cleanup is needed here for this candidate.
_debugLogService?.warn(
'Failed to open $candidate: ${error.msg} (code ${error.error})',
tag: 'USB Serial',
);
lastError = error;
// Try next candidate
} catch (error, stackTrace) {
_status = UsbSerialStatus.disconnected;
_debugLogService?.error(
'Unexpected error opening $candidate: $error\n$stackTrace',
tag: 'USB Serial',
);
rethrow;
}
}
if (!opened) {
_status = UsbSerialStatus.disconnected;
final primary = candidates.first;
final msg = lastError != null
? 'Failed to open USB port $primary: ${lastError.msg} (code ${lastError.error})'
: 'Failed to open USB port $primary';
_debugLogService?.error(msg, tag: 'USB Serial');
throw StateError(msg);
}
}
_connectedPortKey = normalizedPortName;
_connectedPortLabel = normalizedPortName;
if (_useAndroidUsbHost) {
_androidDataSubscription = _androidEventChannel
.receiveBroadcastStream()
.listen(
_handleAndroidData,
onError: _handleSerialError,
onDone: _handleSerialDone,
);
} else {
_dataSubscription = _serial!.onSerialData.stream.listen(
_handleSerialData,
onError: _handleSerialError,
onDone: _handleSerialDone,
);
}
_status = UsbSerialStatus.connected;
}
Future<void> write(Uint8List data) async {
if (!isConnected) {
throw StateError('USB serial port is not open');
}
final packet = wrapUsbSerialTxFrame(data);
_logFrameSummary('USB TX frame', data);
if (_useAndroidUsbHost) {
try {
await _androidMethodChannel.invokeMethod<void>('write', {
'data': packet,
});
} on PlatformException catch (error) {
throw StateError(error.message ?? error.code);
}
} else {
_serial!.write(packet);
}
}
Future<void> disconnect() async {
if (_status == UsbSerialStatus.disconnected) return;
final portLabel = _connectedPortLabel ?? _connectedPortKey;
_debugLogService?.info(
'USB disconnect starting port=${portLabel ?? 'unknown'}',
tag: 'USB Serial',
);
_status = UsbSerialStatus.disconnecting;
_connectedPortKey = null;
_connectedPortLabel = null;
_frameDecoder.reset();
if (_useAndroidUsbHost) {
await _androidDataSubscription?.cancel();
_androidDataSubscription = null;
try {
await _androidMethodChannel.invokeMethod<void>('disconnect');
} catch (_) {
// Ignore errors while closing.
}
} else {
// IMPORTANT: Close and free the native port FIRST, before cancelling the
// Dart subscription. The native SerialThread is blocked on a read(); once
// closePort() is called it unblocks and the thread exits. If we cancel
// the Dart subscription first (freeing the FFI callback pointer) and the
// thread fires one final callback before noticing the port is gone, Dart
// crashes with "Callback invoked after it has been deleted".
final serial = _serial;
_serial = null;
try {
if (serial?.isOpen() == FlOpenStatus.open) {
serial?.closePort();
}
} catch (_) {
// Ignore errors while closing.
}
// Note: we do NOT call free() here; that would globally reset native
// state for all ports. The global reset is done in connect() instead,
// before the next open, which is the safer place to do it.
// Now it is safe to cancel the Dart subscription the native thread has
// already seen the port close and will not fire any more callbacks.
await _dataSubscription?.cancel();
_dataSubscription = null;
}
_status = UsbSerialStatus.disconnected;
_debugLogService?.info(
'USB disconnect complete port=${portLabel ?? 'unknown'}',
tag: 'USB Serial',
);
}
void setRequestPortLabel(String label) {
// Native implementations do not use a synthetic chooser row.
}
void setFallbackDeviceName(String label) {
// Native implementations use OS-provided device names.
}
void updateConnectedLabel(String label) {
final trimmed = label.trim();
if (trimmed.isEmpty) {
return;
}
_connectedPortLabel = buildUsbDisplayLabel(
basePortLabel: _connectedPortKey ?? trimmed,
deviceName: trimmed,
);
}
void dispose() {
// Synchronously close the native port so the SerialThread exits before
// the Dart isolate is torn down (e.g. on hot restart). The async
// disconnect() path via unawaited() offers no ordering guarantee the
// isolate may die before the Future resolves, leaving the thread alive
// with a dangling NativeCallable pointer.
if (_useDesktopFlSerial) {
final serial = _serial;
try {
if (serial?.isOpen() == FlOpenStatus.open) {
serial?.closePort(); // synchronous C call kills the SerialThread
}
} catch (_) {}
}
// Kick off the full async teardown for anything else (subscription cancel,
// stream controller close). These are best-effort at dispose time.
unawaited(disconnect().whenComplete(_closeFrameController));
}
void _handleSerialData(FlSerialEventArgs event) {
try {
final bytes = event.serial.readList();
if (bytes.isNotEmpty) {
_ingestRawBytes(Uint8List.fromList(bytes));
}
} catch (error, stack) {
_addFrameError(error, stack);
}
}
void _handleAndroidData(dynamic data) {
if (data is Uint8List) {
_ingestRawBytes(data);
return;
}
if (data is ByteData) {
_ingestRawBytes(data.buffer.asUint8List());
return;
}
_addFrameError(
StateError('Unexpected Android USB event payload: ${data.runtimeType}'),
);
}
void _handleSerialError(Object error, [StackTrace? stackTrace]) {
_addFrameError(error, stackTrace);
}
void _handleSerialDone() {
unawaited(disconnect());
}
void _ingestRawBytes(Uint8List bytes) {
for (final packet in _frameDecoder.ingest(bytes)) {
if (!packet.isRxFrame) {
_debugLogService?.info(
'USB ignored packet start=0x${packet.frameStart.toRadixString(16).padLeft(2, '0')} len=${packet.payload.length}',
tag: 'USB Serial',
);
continue;
}
_addFrame(packet.payload);
}
}
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);
}
Future<void> _closeFrameController() async {
if (_frameController.isClosed) {
return;
}
await _frameController.close();
}
void _logFrameSummary(String prefix, Uint8List bytes) {
if (bytes.isEmpty) {
_debugLogService?.info('$prefix len=0', tag: 'USB Serial');
return;
}
_debugLogService?.info(
'$prefix code=${bytes[0]} len=${bytes.length}',
tag: 'USB Serial',
);
}
/// Returns an ordered list of port paths to try for [portName].
///
/// On macOS, USB serial devices appear as both `/dev/cu.*` (call-out, the
/// correct mode for outgoing serial connections) and `/dev/tty.*` (dial-in).
/// `flserial` may list one variant while only the other is actually openable
/// at a given moment. We prefer `cu.*` but automatically include the `tty.*`
/// sibling as a fallback, and vice-versa.
List<String> _buildPortCandidates(String normalizedPort) {
if (!Platform.isMacOS) return [normalizedPort];
const cuPrefix = '/dev/cu.';
const ttyPrefix = '/dev/tty.';
if (normalizedPort.startsWith(cuPrefix)) {
final suffix = normalizedPort.substring(cuPrefix.length);
return [normalizedPort, '$ttyPrefix$suffix'];
}
if (normalizedPort.startsWith(ttyPrefix)) {
final suffix = normalizedPort.substring(ttyPrefix.length);
return [normalizedPort, '$cuPrefix$suffix'];
}
return [normalizedPort];
}
}
enum UsbSerialStatus { disconnected, connecting, connected, disconnecting }
-580
View File
@@ -1,580 +0,0 @@
import 'dart:async';
import 'dart:js_interop';
import 'dart:js_interop_unsafe';
import 'package:flutter/foundation.dart';
import 'package:web/web.dart' as web;
import 'app_debug_log_service.dart';
import '../utils/usb_port_labels.dart';
import 'usb_serial_frame_codec.dart';
class UsbSerialService {
UsbSerialService();
static const Map<String, String> _knownUsbNames = <String, String>{
'2886:1667': 'Seeed Wio Tracker L1',
};
static final Map<String, String> _deviceNamesByPortKey = <String, String>{};
static final Map<String, String> _baseLabelsByPortKey = <String, String>{};
static final Map<String, JSObject> _authorizedPortsByKey =
<String, JSObject>{};
static int _nextAuthorizedPortId = 1;
final StreamController<Uint8List> _frameController =
StreamController<Uint8List>.broadcast();
final UsbSerialFrameDecoder _frameDecoder = UsbSerialFrameDecoder();
UsbSerialStatus _status = UsbSerialStatus.disconnected;
JSObject? _port;
JSObject? _reader;
JSObject? _writer;
String? _connectedPortName;
String? _connectedPortKey;
String _requestPortLabel = 'Choose USB Device';
String _fallbackDeviceName = 'Web Serial Device';
AppDebugLogService? _debugLogService;
UsbSerialStatus get status => _status;
String? get activePortKey => _connectedPortKey;
String? get activePortDisplayLabel => _connectedPortName ?? _connectedPortKey;
Stream<Uint8List> get frameStream => _frameController.stream;
bool get isConnected => _status == UsbSerialStatus.connected;
JSObject get _navigator => JSObject.fromInteropObject(web.window.navigator);
bool get _isSupported => _navigator.has('serial');
JSObject? get _serial {
if (!_isSupported) {
return null;
}
final serial = _navigator['serial'];
return serial == null ? null : serial as JSObject;
}
Future<List<String>> listPorts() async {
if (!_isSupported) {
return const <String>[];
}
_resetPortCache();
final ports = await _getAuthorizedPorts();
return <String>[_requestPortListEntry, ...ports.map(_listEntryForPort)];
}
Future<void> connect({
required String portName,
int baudRate = 115200,
}) async {
if (_status == UsbSerialStatus.connected ||
_status == UsbSerialStatus.connecting) {
throw StateError('USB serial transport is already active');
}
if (!_isSupported) {
throw UnsupportedError('Web Serial is not supported by this browser.');
}
_status = UsbSerialStatus.connecting;
_frameDecoder.reset();
try {
final requestedPortName = normalizeUsbPortName(portName);
_debugLogService?.info(
'Web connect: requested=$requestedPortName baud=$baudRate',
tag: 'USB Serial',
);
final selectedPortKey = requestedPortName.startsWith('web:port:')
? requestedPortName
: null;
_port = _authorizedPortsByKey[requestedPortName];
final authorizedPorts = await _getAuthorizedPorts();
_debugLogService?.info(
'Web connect: ${authorizedPorts.length} authorized port(s), cached=${_port != null}',
tag: 'USB Serial',
);
_port ??= _selectPort(authorizedPorts, requestedPortName);
_port ??= await _requestPort();
if (_port == null) {
throw StateError('No USB serial device selected');
}
_debugLogService?.info(
'Web connect: opening port at $baudRate baud…',
tag: 'USB Serial',
);
await _openPort(_port!, baudRate);
_connectedPortKey = _cachePort(_port!, preferredKey: selectedPortKey);
_connectedPortName = _displayLabelForPort(
_port!,
portKey: _connectedPortKey,
);
_writer = _getWriter(_port!);
_reader = _getReader(_port!);
_status = UsbSerialStatus.connected;
unawaited(_pumpReads());
_debugLogService?.info(
'USB serial opened port=$_connectedPortName via Web Serial',
tag: 'USB Serial',
);
} catch (error) {
_debugLogService?.error('Web connect failed: $error', tag: 'USB Serial');
await _cleanupFailedConnect();
_status = UsbSerialStatus.disconnected;
_connectedPortName = null;
_connectedPortKey = null;
rethrow;
}
}
Future<void> write(Uint8List data) async {
if (!isConnected || _writer == null) {
throw StateError('USB serial port is not open');
}
final packet = wrapUsbSerialTxFrame(data);
_logFrameSummary('USB TX frame', data);
final promise = _writer!.callMethod<JSPromise<JSAny?>>(
'write'.toJS,
packet.toJS,
);
await promise.toDart;
}
Future<void> disconnect() async {
if (_status == UsbSerialStatus.disconnected) return;
final portLabel = _connectedPortName ?? _connectedPortKey;
_debugLogService?.info(
'USB disconnect starting port=${portLabel ?? 'unknown'}',
tag: 'USB Serial',
);
_status = UsbSerialStatus.disconnecting;
final reader = _reader;
final writer = _writer;
final port = _port;
_reader = null;
_writer = null;
_port = null;
_connectedPortName = null;
_connectedPortKey = null;
_frameDecoder.reset();
if (reader != null) {
try {
await reader.callMethod<JSPromise<JSAny?>>('cancel'.toJS).toDart;
} catch (_) {
// Ignore errors while closing.
}
_releaseLock(reader);
}
if (writer != null) {
_releaseLock(writer);
}
if (port != null) {
try {
await port.callMethod<JSPromise<JSAny?>>('close'.toJS).toDart;
} catch (_) {
// Ignore errors while closing.
}
}
_status = UsbSerialStatus.disconnected;
_debugLogService?.info(
'USB disconnect complete port=${portLabel ?? 'unknown'}',
tag: 'USB Serial',
);
}
void updateConnectedLabel(String label) {
final trimmed = label.trim();
final portKey = _connectedPortKey;
if (trimmed.isEmpty || portKey == null) {
return;
}
_deviceNamesByPortKey[portKey] = trimmed;
_connectedPortName = _buildDisplayLabel(portKey);
}
void setRequestPortLabel(String label) {
final trimmed = label.trim();
if (trimmed.isEmpty) {
return;
}
_requestPortLabel = trimmed;
}
void setFallbackDeviceName(String label) {
final trimmed = label.trim();
if (trimmed.isEmpty) {
return;
}
_fallbackDeviceName = trimmed;
}
void setDebugLogService(AppDebugLogService? service) {
_debugLogService = service;
}
void dispose() {
unawaited(disconnect().whenComplete(_closeFrameController));
}
Future<List<JSObject>> _getAuthorizedPorts() async {
final serial = _serial;
if (serial == null) {
return const <JSObject>[];
}
final result = await serial
.callMethod<JSPromise<JSAny?>>('getPorts'.toJS)
.toDart;
return _toObjectList(result);
}
Future<JSObject?> _requestPort() async {
final serial = _serial;
if (serial == null) {
return null;
}
final result = await serial
.callMethod<JSPromise<JSAny?>>('requestPort'.toJS)
.toDart;
return result == null ? null : result as JSObject;
}
JSObject? _selectPort(List<JSObject> ports, String requestedPortName) {
if (ports.isEmpty) {
return null;
}
if (requestedPortName.isEmpty || requestedPortName == _requestPortKey) {
return ports.first;
}
if (requestedPortName.startsWith('web:port:')) {
return null;
}
for (final port in ports) {
final description = _describePort(port);
if (description == requestedPortName) {
return port;
}
}
return null;
}
Future<void> _openPort(JSObject port, int baudRate) async {
final options = JSObject()
..['baudRate'] = baudRate.toJS
..['flowControl'] = 'none'.toJS;
await port.callMethod<JSPromise<JSAny?>>('open'.toJS, options).toDart;
// Prevent ESP32 USB-CDC reset: hold DTR=true, RTS=false after open.
try {
final signals = JSObject()
..['dataTerminalReady'] = true.toJS
..['requestToSend'] = false.toJS;
await port
.callMethod<JSPromise<JSAny?>>('setSignals'.toJS, signals)
.toDart;
} catch (_) {
// setSignals may not be supported on all browsers/devices.
}
}
Future<void> _cleanupFailedConnect() async {
final reader = _reader;
final writer = _writer;
final port = _port;
_reader = null;
_writer = null;
_port = null;
if (reader != null) {
try {
await reader.callMethod<JSPromise<JSAny?>>('cancel'.toJS).toDart;
} catch (_) {
// Ignore cleanup errors after a failed connect.
}
_releaseLock(reader);
}
if (writer != null) {
_releaseLock(writer);
}
if (port != null) {
try {
await port.callMethod<JSPromise<JSAny?>>('close'.toJS).toDart;
} catch (_) {
// Ignore cleanup errors after a failed connect.
}
}
}
JSObject? _getReader(JSObject port) {
final readable = port.getProperty<JSAny?>('readable'.toJS);
if (readable == null) {
throw StateError('Web Serial port is not readable');
}
final readableObject = readable as JSObject;
return readableObject.callMethod<JSAny?>('getReader'.toJS) as JSObject;
}
JSObject? _getWriter(JSObject port) {
final writable = port.getProperty<JSAny?>('writable'.toJS);
if (writable == null) {
throw StateError('Web Serial port is not writable');
}
final writableObject = writable as JSObject;
return writableObject.callMethod<JSAny?>('getWriter'.toJS) as JSObject;
}
Future<void> _pumpReads() async {
final reader = _reader;
if (reader == null) {
_debugLogService?.warn('_pumpReads: reader is null', tag: 'USB Serial');
return;
}
_debugLogService?.info('_pumpReads: started', tag: 'USB Serial');
try {
while (_status == UsbSerialStatus.connected &&
identical(reader, _reader)) {
final result = await reader
.callMethod<JSPromise<JSAny?>>('read'.toJS)
.toDart;
if (result == null) {
_debugLogService?.warn('_pumpReads: null result', tag: 'USB Serial');
break;
}
final resultObject = result as JSObject;
final doneValue = resultObject.getProperty<JSAny?>('done'.toJS);
final done = doneValue != null && doneValue.dartify() == true;
if (done) {
_debugLogService?.info('_pumpReads: done=true', tag: 'USB Serial');
break;
}
final value = resultObject.getProperty<JSAny?>('value'.toJS);
final bytes = _coerceBytes(value);
if (bytes != null && bytes.isNotEmpty) {
_debugLogService?.info(
'USB RX raw: ${bytes.length} byte(s)',
tag: 'USB Serial',
);
_ingestRawBytes(bytes);
}
}
} catch (error, stackTrace) {
_debugLogService?.error('_pumpReads error: $error', tag: 'USB Serial');
if (_status == UsbSerialStatus.connected) {
_addFrameError(error, stackTrace);
}
} finally {
_debugLogService?.info('_pumpReads: ended', tag: 'USB Serial');
_releaseLock(reader);
if (_status == UsbSerialStatus.connected && identical(reader, _reader)) {
_addFrameError(StateError('USB serial connection closed'));
}
}
}
Uint8List? _coerceBytes(JSAny? value) {
if (value == null) return null;
try {
return (value as JSUint8Array).toDart;
} catch (_) {
// Fall back to array-like coercion below.
}
final object = value as JSObject;
if (object.has('length')) {
final lengthValue = object.getProperty<JSAny?>('length'.toJS)?.dartify();
if (lengthValue is num) {
final length = lengthValue.toInt();
final bytes = Uint8List(length);
for (var i = 0; i < length; i++) {
final item = object.getProperty<JSAny?>(i.toString().toJS)?.dartify();
if (item is num) {
bytes[i] = item.toInt();
}
}
return bytes;
}
}
return null;
}
List<JSObject> _toObjectList(JSAny? value) {
if (value == null) {
return const <JSObject>[];
}
final object = value as JSObject;
if (!object.has('length')) {
return const <JSObject>[];
}
final lengthValue = object.getProperty<JSAny?>('length'.toJS)?.dartify();
if (lengthValue is! num) {
return const <JSObject>[];
}
final length = lengthValue.toInt();
final items = <JSObject>[];
for (var i = 0; i < length; i++) {
final item = object.getProperty<JSAny?>(i.toString().toJS);
if (item != null) {
items.add(item as JSObject);
}
}
return items;
}
String _describePort(JSObject port) {
final info = _portInfo(port);
if (info == null) {
return _requestPortLabel;
}
final vendorId = info.usbVendorId;
final productId = info.usbProductId;
final hasVendor = vendorId != null;
final hasProduct = productId != null;
return describeWebUsbPort(
vendorId: hasVendor ? vendorId : null,
productId: hasProduct ? productId : null,
requestPortLabel: _requestPortLabel,
fallbackDeviceName: _fallbackDeviceName,
knownUsbNames: _knownUsbNames,
);
}
_WebPortInfo? _portInfo(JSObject port) {
try {
final info = port.callMethod<JSAny?>('getInfo'.toJS);
if (info == null) {
return null;
}
final infoObject = info as JSObject;
final vendorId = infoObject
.getProperty<JSAny?>('usbVendorId'.toJS)
?.dartify();
final productId = infoObject
.getProperty<JSAny?>('usbProductId'.toJS)
?.dartify();
return _WebPortInfo(
usbVendorId: vendorId is num ? vendorId.toInt() : null,
usbProductId: productId is num ? productId.toInt() : null,
);
} catch (_) {
return null;
}
}
String _portKeyFor(JSObject port) {
return _cachePort(port);
}
String _cachePort(JSObject port, {String? preferredKey}) {
final portKey = preferredKey ?? 'web:port:${_nextAuthorizedPortId++}';
_baseLabelsByPortKey[portKey] = _describePort(port);
_authorizedPortsByKey[portKey] = port;
return portKey;
}
String _displayLabelForPort(JSObject port, {String? portKey}) =>
_buildDisplayLabel(portKey ?? _portKeyFor(port));
String _buildDisplayLabel(String portKey) {
return buildUsbDisplayLabel(
basePortLabel: _baseLabelsByPortKey[portKey] ?? portKey,
deviceName: _deviceNamesByPortKey[portKey],
);
}
String _listEntryForPort(JSObject port) {
final portKey = _portKeyFor(port);
return '$portKey - ${_displayLabelForPort(port, portKey: portKey)}';
}
String get _requestPortKey => 'web:request';
String get _requestPortListEntry => '$_requestPortKey - $_requestPortLabel';
void _resetPortCache() {
_authorizedPortsByKey.clear();
_baseLabelsByPortKey.clear();
_deviceNamesByPortKey.clear();
_nextAuthorizedPortId = 1;
}
void _releaseLock(JSObject resource) {
try {
resource.callMethod<JSAny?>('releaseLock'.toJS);
} catch (_) {
// Ignore lock release failures.
}
}
void _ingestRawBytes(Uint8List bytes) {
for (final packet in _frameDecoder.ingest(bytes)) {
if (!packet.isRxFrame) {
_debugLogService?.info(
'USB ignored packet start=0x${packet.frameStart.toRadixString(16).padLeft(2, '0')} len=${packet.payload.length}',
tag: 'USB Serial',
);
continue;
}
_addFrame(packet.payload);
}
}
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);
}
Future<void> _closeFrameController() async {
if (_frameController.isClosed) {
return;
}
await _frameController.close();
}
void _logFrameSummary(String prefix, Uint8List bytes) {
if (bytes.isEmpty) {
_debugLogService?.info('$prefix len=0', tag: 'USB Serial');
return;
}
_debugLogService?.info(
'$prefix code=${bytes[0]} len=${bytes.length}',
tag: 'USB Serial',
);
}
}
enum UsbSerialStatus { disconnected, connecting, connected, disconnecting }
final class _WebPortInfo {
const _WebPortInfo({required this.usbVendorId, required this.usbProductId});
final int? usbVendorId;
final int? usbProductId;
}
+7 -44
View File
@@ -1,7 +1,5 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:meshcore_open/utils/app_logger.dart';
import '../models/channel_message.dart';
import '../helpers/smaz.dart';
import 'prefs_manager.dart';
@@ -9,25 +7,13 @@ import 'prefs_manager.dart';
class ChannelMessageStore {
static const String _keyPrefix = 'channel_messages_';
String publicKeyHex = '';
set setPublicKeyHex(String value) =>
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
String get keyFor => '$_keyPrefix$publicKeyHex';
/// Save messages for a specific channel
Future<void> saveChannelMessages(
int channelIndex,
List<ChannelMessage> messages,
) async {
if (publicKeyHex.isEmpty) {
appLogger.warn(
'Public key hex is not set. Cannot save channel messages.',
);
return;
}
final prefs = PrefsManager.instance;
final key = '$keyFor$channelIndex';
final key = '$_keyPrefix$channelIndex';
// Convert messages to JSON
final jsonList = messages.map((msg) => _messageToJson(msg)).toList();
@@ -38,35 +24,12 @@ class ChannelMessageStore {
/// Load messages for a specific channel
Future<List<ChannelMessage>> loadChannelMessages(int channelIndex) async {
if (publicKeyHex.isEmpty) {
appLogger.warn(
'Public key hex is not set. Cannot load channel messages.',
);
return [];
}
final prefs = PrefsManager.instance;
final key = '$keyFor$channelIndex';
final oldKey = '$_keyPrefix$channelIndex';
final key = '$_keyPrefix$channelIndex';
final jsonString = prefs.getString(key);
if (jsonString == null) return [];
String? jsonString = prefs.getString(key);
if (jsonString == null || jsonString.isEmpty) {
// Attempt migration from legacy unscoped key on first load
final legacyJsonString = prefs.getString(oldKey);
prefs.remove(oldKey);
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
appLogger.info(
'Migrating channel messages from legacy key $oldKey to scoped key $key',
);
await prefs.setString(key, legacyJsonString);
jsonString = legacyJsonString;
}
}
if (jsonString == null || jsonString.isEmpty) {
jsonString = prefs.getString(keyFor);
}
if (jsonString == null || jsonString.isEmpty) {
return [];
}
try {
final jsonList = jsonDecode(jsonString) as List<dynamic>;
return jsonList.map((json) => _messageFromJson(json)).toList();
@@ -79,14 +42,14 @@ class ChannelMessageStore {
/// Clear messages for a specific channel
Future<void> clearChannelMessages(int channelIndex) async {
final prefs = PrefsManager.instance;
final key = '$keyFor$channelIndex';
final key = '$_keyPrefix$channelIndex';
await prefs.remove(key);
}
/// Clear all channel messages
Future<void> clearAllChannelMessages() async {
final prefs = PrefsManager.instance;
final keys = prefs.getKeys().where((k) => k.startsWith(keyFor));
final keys = prefs.getKeys().where((k) => k.startsWith(_keyPrefix));
for (var key in keys) {
await prefs.remove(key);
}
+6 -35
View File
@@ -1,49 +1,20 @@
import 'dart:convert';
import '../utils/app_logger.dart';
import 'prefs_manager.dart';
class ChannelOrderStore {
static const String _keyPrefix = 'channel_order_';
String publicKeyHex = '';
set setPublicKeyHex(String value) =>
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
String get keyFor => '$_keyPrefix$publicKeyHex';
static const String _key = 'channel_order';
Future<void> saveChannelOrder(List<int> order) async {
if (publicKeyHex.isEmpty) {
appLogger.warn('Public key hex is not set. Cannot save channel order.');
return;
}
final prefs = PrefsManager.instance;
await prefs.setString(keyFor, jsonEncode(order));
await prefs.setString(_key, jsonEncode(order));
}
Future<List<int>> loadChannelOrder() async {
if (publicKeyHex.isEmpty) {
appLogger.warn('Public key hex is not set. Cannot load channel order.');
return [];
}
final prefs = PrefsManager.instance;
String? jsonString = prefs.getString(keyFor);
if (jsonString == null || jsonString.isEmpty) {
// Attempt migration from legacy unscoped key on first load
final legacyJsonString = prefs.getString(_keyPrefix);
prefs.remove(_keyPrefix);
if (legacyJsonString != null && legacyJsonString.isNotEmpty) {
appLogger.info(
'Migrating channel order from legacy key $_keyPrefix to scoped key $keyFor',
);
await prefs.setString(keyFor, legacyJsonString);
jsonString = legacyJsonString;
}
}
if (jsonString == null || jsonString.isEmpty) {
return [];
}
final raw = prefs.getString(_key);
if (raw == null || raw.isEmpty) return [];
try {
final decoded = jsonDecode(jsonString);
final decoded = jsonDecode(raw);
if (decoded is List) {
return decoded
.map((value) => value is int ? value : int.tryParse('$value'))
@@ -53,7 +24,7 @@ class ChannelOrderStore {
} catch (_) {
// fall through to legacy parse
}
return jsonString
return raw
.split(',')
.map((value) => int.tryParse(value))
.whereType<int>()
+4 -36
View File
@@ -1,49 +1,17 @@
import '../utils/app_logger.dart';
import 'prefs_manager.dart';
class ChannelSettingsStore {
static const String _keyPrefix = 'channel_smaz_';
String publicKeyHex = '';
set setPublicKeyHex(String value) =>
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
String get keyFor => '$_keyPrefix$publicKeyHex';
static const String _smazKeyPrefix = 'channel_smaz_';
Future<bool> loadSmazEnabled(int channelIndex) async {
if (publicKeyHex.isEmpty) {
appLogger.warn(
'Public key hex is not set. Cannot load channel settings.',
);
return false;
}
final prefs = PrefsManager.instance;
final key = '$keyFor$channelIndex';
final oldKey = '$_keyPrefix$channelIndex';
bool? enabled = prefs.getBool(oldKey);
if (enabled == null) {
// Attempt migration from legacy unscoped key on first load
enabled = prefs.getBool(oldKey);
prefs.remove(oldKey);
if (enabled != null) {
appLogger.info(
'Migrating channel settings from legacy key $oldKey to scoped key $key',
);
await prefs.setBool(key, enabled);
}
}
return enabled ?? false;
final key = '$_smazKeyPrefix$channelIndex';
return prefs.getBool(key) ?? false;
}
Future<void> saveSmazEnabled(int channelIndex, bool enabled) async {
if (publicKeyHex.isEmpty) {
appLogger.warn(
'Public key hex is not set. Cannot save channel settings.',
);
return;
}
final prefs = PrefsManager.instance;
final key = '$keyFor$channelIndex';
final key = '$_smazKeyPrefix$channelIndex';
await prefs.setBool(key, enabled);
}
}

Some files were not shown because too many files have changed in this diff Show More