mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-20 01:15:35 +10:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0135d56ddc |
@@ -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
|
||||
@@ -83,6 +83,3 @@ keystore.properties
|
||||
# IDE
|
||||
.vscode/launch.json
|
||||
.vscode/settings.json
|
||||
|
||||
# Cloudflare Wrangler
|
||||
.wrangler
|
||||
@@ -1 +0,0 @@
|
||||
6.2.4
|
||||
@@ -51,7 +51,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
||||
|
||||
### Device Management
|
||||
|
||||
- **BLE, USB, TCP Connection**: Scan and connect to MeshCore devices via Bluetooth, USB or TCP
|
||||
- **BLE Connection**: Scan and connect to MeshCore devices via Bluetooth
|
||||
- **Device Settings**: Configure radio parameters, power settings, and network options
|
||||
- **Battery Monitoring**: Real-time battery status with chemistry-specific voltage curves
|
||||
- **Firmware Updates**: Over-the-air firmware updates via BLE (coming soon)
|
||||
@@ -75,16 +75,10 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
||||
|
||||
### Platform Support
|
||||
|
||||
| Feature | Android (API 21+) | iOS (12+) | Linux | Windows | macOS | Web |
|
||||
|--------------------|:-----------------:|:---------:|:-----:|:-------:|:-----:|:---------------------------------:|
|
||||
| BLE companion | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| USB companion | ✅ | 🚧 | ✅ | ✅ | ✅ | ✅ |
|
||||
| TCP companion | ✅ | 🚧 | ✅ | ✅ | ✅ | ❌<br>(requires websocket bridge) |
|
||||
| Core Functionality | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Mesh Network | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Map & Location | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Device Management | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Repeater Hub | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
- ✅ **Android**: Full support (API 21+)
|
||||
- ✅ **iOS**: Full support (iOS 12+)
|
||||
- 🚧 **Desktop**: Limited support (macOS/Linux/Windows)
|
||||
- 🚧 **Web**: Under construction (Chrome)
|
||||
|
||||
### Dependencies
|
||||
|
||||
@@ -195,7 +189,6 @@ Messages are transmitted as binary frames using a custom protocol optimized for
|
||||
### App Settings
|
||||
|
||||
- **Theme**: System default, light, or dark mode
|
||||
- **Language**: Use one of 15 languages (English, Chinese, French, Spanish, Portuguese, German, Dutch, Polish, Swedish, Italian, Slovak, Slovene, Bulgarian, Russian, Ukrainian)
|
||||
- **Notifications**: Configurable for messages, channels, and node advertisements
|
||||
- **Battery Chemistry**: Support for NMC, LiFePO4, and LiPo battery types
|
||||
- **Message Retry**: Automatic retry with configurable path clearing
|
||||
@@ -238,11 +231,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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 104 KiB |
+249
-1449
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,78 +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);
|
||||
|
||||
Future<void> writeRaw(Uint8List data) => _service.writeRaw(data);
|
||||
|
||||
// --- Label management ---
|
||||
void updateConnectedLabel(String selfName) {
|
||||
_service.updateConnectedLabel(selfName);
|
||||
_activePortLabel = _service.activePortDisplayLabel ?? _activePortLabel;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_service.dispose();
|
||||
}
|
||||
}
|
||||
@@ -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,7 +13,6 @@ 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}',
|
||||
@@ -26,7 +24,6 @@ class BufferReader {
|
||||
}
|
||||
|
||||
void skipBytes(int count) {
|
||||
_lastPointer = _pointer;
|
||||
if (_pointer + count > _buffer.length) {
|
||||
throw RangeError(
|
||||
'Attempted to skip $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
|
||||
@@ -37,18 +34,10 @@ class BufferReader {
|
||||
|
||||
Uint8List readRemainingBytes() => readBytes(remaining);
|
||||
|
||||
String readString() {
|
||||
_lastPointer = _pointer;
|
||||
final value = readRemainingBytes();
|
||||
try {
|
||||
return utf8.decode(Uint8List.fromList(value), allowMalformed: true);
|
||||
} catch (e) {
|
||||
return String.fromCharCodes(value); // Latin-1 fallback
|
||||
}
|
||||
}
|
||||
String 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 +51,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 +72,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 +114,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 +162,11 @@ const int cmdGetChannel = 31;
|
||||
const int cmdSetChannel = 32;
|
||||
const int cmdSendTracePath = 36;
|
||||
const int cmdSetOtherParams = 38;
|
||||
const int cmdSendAnonReq = 57;
|
||||
const int cmdSendTelemetryReq = 39;
|
||||
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;
|
||||
@@ -249,8 +200,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;
|
||||
@@ -272,10 +223,6 @@ const int advTypeRepeater = 2;
|
||||
const int advTypeRoom = 3;
|
||||
const int advTypeSensor = 4;
|
||||
|
||||
const int teleModeDeny = 0;
|
||||
const int teleModeAllowFlags = 1; // use contact.flags
|
||||
const int teleModeAllowAll = 2;
|
||||
|
||||
// Payload Types
|
||||
const int payloadTypeREQ =
|
||||
0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
|
||||
@@ -300,18 +247,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;
|
||||
@@ -356,16 +291,13 @@ const int contactPubKeyOffset = 1;
|
||||
const int contactTypeOffset = 33;
|
||||
const int contactFlagsOffset = 34;
|
||||
const int contactFlagFavorite = 0x01;
|
||||
const int contactFlagTeleBase = 0x02; // 'base' permission includes battery
|
||||
const int contactFlagTeleLoc = 0x04;
|
||||
const int contactFlagTeleEnv = 0x08; //access environment sensors
|
||||
const int contactPathLenOffset = 35;
|
||||
const int contactPathOffset = 36;
|
||||
const int contactNameOffset = 100;
|
||||
const int contactTimestampOffset = 132;
|
||||
const int contactLatOffset = 136;
|
||||
const int contactLonOffset = 140;
|
||||
const int contactLastModOffset = 144;
|
||||
const int contactLastmodOffset = 144;
|
||||
const int contactFrameSize = 148;
|
||||
|
||||
// Message frame offsets
|
||||
@@ -696,17 +628,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);
|
||||
@@ -715,7 +644,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);
|
||||
@@ -724,27 +663,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();
|
||||
}
|
||||
|
||||
@@ -757,15 +675,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
|
||||
@@ -890,10 +809,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();
|
||||
}
|
||||
|
||||
@@ -907,55 +826,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();
|
||||
}
|
||||
|
||||
//Build CMD_SEND_TELEMETRY_REQ
|
||||
// Format: [cmd][reserved x3][pub_key? x32]
|
||||
Uint8List buildSendTelemetryReq(Uint8List? pubKey) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdSendTelemetryReq);
|
||||
|
||||
if (pubKey != null && pubKey.length == pubKeySize) {
|
||||
writer.writeBytes(Uint8List(3)); // reserved bytes
|
||||
writer.writeBytes(pubKey);
|
||||
} else {
|
||||
writer.writeBytes(Uint8List(4)); // reserved bytes
|
||||
}
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
+1
-122
@@ -285,7 +285,6 @@
|
||||
"contacts_newGroup": "Нова група",
|
||||
"contacts_groupName": "Група",
|
||||
"contacts_groupNameRequired": "Името на групата е задължително.",
|
||||
"contacts_groupNameReserved": "Това име на група е запазено",
|
||||
"contacts_groupAlreadyExists": "Групата \"{name}\" вече съществува.",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -1607,8 +1606,6 @@
|
||||
"scanner_bluetoothOff": "Bluetooth е изключен.",
|
||||
"scanner_enableBluetooth": "Активирайте Bluetooth",
|
||||
"scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.",
|
||||
"scanner_chromeRequired": "Изисква се браузър Chrome",
|
||||
"scanner_chromeRequiredMessage": "Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.",
|
||||
"snrIndicator_lastSeen": "Последно видян",
|
||||
"snrIndicator_nearByRepeaters": "Близки повтарящи се устройства",
|
||||
"chat_ShowAllPaths": "Покажи всички пътища",
|
||||
@@ -1802,123 +1799,5 @@
|
||||
"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": "Задайте като моя местоположение",
|
||||
"settings_denyAll": "Откажи всичко",
|
||||
"settings_allowAll": "Позволи всичко",
|
||||
"settings_allowByContact": "Позволи по флагове за контакт",
|
||||
"settings_privacy": "Настройки на поверителността",
|
||||
"settings_privacySettingsDescription": "Изберете каква информация устройството ви споделя с другите.",
|
||||
"settings_privacySubtitle": "Контролирайте каква информация се споделя.",
|
||||
"settings_telemetryBaseMode": "Базов режим на телеметрия",
|
||||
"settings_telemetryLocationMode": "Режим на местоположение на телеметрията",
|
||||
"settings_advertLocation": "Място на обявата",
|
||||
"settings_advertLocationSubtitle": "Включи местоположение в обявата",
|
||||
"contact_info": "Контактна информация",
|
||||
"settings_telemetryEnvironmentMode": "Режим на средата на телеметрията",
|
||||
"contact_telemetry": "Телеметрия",
|
||||
"contact_lastSeen": "Последно видян",
|
||||
"contact_clearChat": "Изчисти чата",
|
||||
"contact_teleBase": "Базата данни за телеметрия",
|
||||
"contact_settings": "Настройки за контакти",
|
||||
"contact_teleBaseSubtitle": "Позволи споделяне на ниво на батерията и основна телеметрия",
|
||||
"contact_teleEnv": "Среда на телеметрия",
|
||||
"contact_teleLocSubtitle": "Позволи споделяне на данни за местоположение",
|
||||
"contact_teleLoc": "Местоположение на телеметрията",
|
||||
"contact_teleEnvSubtitle": "Позволи споделяне на данни от средносферните датчици",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_multiAck": "Мулти-потвърди: {value}",
|
||||
"settings_telemetryModeUpdated": "Режим на телеметрията е обновен"
|
||||
"contacts_searchUsers": "Търсене на {number}{str} потребители..."
|
||||
}
|
||||
|
||||
+7
-128
@@ -285,7 +285,6 @@
|
||||
"contacts_newGroup": "Neue Gruppe",
|
||||
"contacts_groupName": "Gruppenname",
|
||||
"contacts_groupNameRequired": "Der Gruppennamen ist erforderlich.",
|
||||
"contacts_groupNameReserved": "Dieser Gruppenname ist reserviert",
|
||||
"contacts_groupAlreadyExists": "Die Gruppe \"{name}\" existiert bereits.",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -297,8 +296,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": {
|
||||
@@ -306,8 +305,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": {
|
||||
@@ -315,8 +314,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": {
|
||||
@@ -1636,8 +1635,6 @@
|
||||
"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",
|
||||
@@ -1830,123 +1827,5 @@
|
||||
"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",
|
||||
"settings_allowByContact": "Zulassen durch Kontaktflaggen",
|
||||
"settings_privacy": "Datenschutzeinstellungen",
|
||||
"settings_allowAll": "Alles zulassen",
|
||||
"settings_privacySettingsDescription": "Wählen Sie die Informationen, die Ihr Gerät mit anderen teilt.",
|
||||
"settings_denyAll": "Alle ablehnen",
|
||||
"settings_privacySubtitle": "Steuern Sie die Informationen, die freigegeben werden.",
|
||||
"settings_telemetryLocationMode": "Telemetrie-Ortsmodus",
|
||||
"settings_telemetryEnvironmentMode": "Telemetrie-Umgebungsmodus",
|
||||
"settings_advertLocation": "Anzeigenort",
|
||||
"settings_advertLocationSubtitle": "Ort in der Anzeige einbeziehen",
|
||||
"settings_telemetryBaseMode": "Telemetrie-Basismodus",
|
||||
"contact_teleBase": "Telemetriebasis",
|
||||
"contact_teleBaseSubtitle": "Erlauben des Freigebens des Batteriestands und der grundlegenden Telemetrie",
|
||||
"contact_teleLoc": "Telemetrieort",
|
||||
"contact_teleLocSubtitle": "Teilen von Standortdaten zulassen",
|
||||
"contact_info": "Kontaktinformationen",
|
||||
"contact_settings": "Kontakteinstellungen",
|
||||
"contact_telemetry": "Telemetrie",
|
||||
"contact_teleEnv": "Telemetrieumgebung",
|
||||
"contact_lastSeen": "Zuletzt gesehen",
|
||||
"contact_clearChat": "Chat löschen",
|
||||
"contact_teleEnvSubtitle": "Teilen von Umgebungsensordaten zulassen",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_telemetryModeUpdated": "Telemetriemodus aktualisiert",
|
||||
"settings_multiAck": "Mehrfach-Bestätigungen: {value}"
|
||||
"contacts_searchRoomServers": "Suche {number}{str} Raumserver..."
|
||||
}
|
||||
|
||||
+8
-129
@@ -10,7 +10,6 @@
|
||||
"common_unknownDevice": "Unknown Device",
|
||||
"common_save": "Save",
|
||||
"common_delete": "Delete",
|
||||
"common_deleteAll": "Delete All",
|
||||
"common_close": "Close",
|
||||
"common_edit": "Edit",
|
||||
"common_add": "Add",
|
||||
@@ -47,64 +46,6 @@
|
||||
}
|
||||
},
|
||||
"scanner_title": "MeshCore Open",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"connectionChoiceTcpLabel": "TCP",
|
||||
"tcpScreenTitle": "Connect over TCP",
|
||||
"tcpHostLabel": "IP Address",
|
||||
"tcpHostHint": "192.168.40.10",
|
||||
"tcpPortLabel": "Port",
|
||||
"tcpPortHint": "5000",
|
||||
"tcpStatus_notConnected": "Enter endpoint and connect",
|
||||
"tcpStatus_connectingTo": "Connecting to {endpoint}...",
|
||||
"@tcpStatus_connectingTo": {
|
||||
"placeholders": {
|
||||
"endpoint": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tcpErrorHostRequired": "IP address is required.",
|
||||
"tcpErrorPortInvalid": "Port must be between 1 and 65535.",
|
||||
"tcpErrorUnsupported": "TCP transport is not supported on this platform.",
|
||||
"tcpErrorTimedOut": "TCP connection timed out.",
|
||||
"tcpConnectionFailed": "TCP connection failed: {error}",
|
||||
"@tcpConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"usbScreenTitle": "Connect over USB",
|
||||
"usbScreenSubtitle": "Choose a detected serial device and connect directly to your MeshCore node.",
|
||||
"usbScreenStatus": "Select a USB device",
|
||||
"usbScreenNote": "USB serial is active on supported Android devices and desktop platforms.",
|
||||
"usbScreenEmptyState": "No USB devices found. Plug one in and refresh.",
|
||||
"usbErrorPermissionDenied": "USB permission was denied.",
|
||||
"usbErrorDeviceMissing": "The selected USB device is no longer available.",
|
||||
"usbErrorInvalidPort": "Select a valid USB device.",
|
||||
"usbErrorBusy": "Another USB connection request is already in progress.",
|
||||
"usbErrorNotConnected": "No USB device is connected.",
|
||||
"usbErrorOpenFailed": "Failed to open the selected USB device.",
|
||||
"usbErrorConnectFailed": "Failed to connect to the selected USB device.",
|
||||
"usbErrorUnsupported": "USB serial is not supported on this platform.",
|
||||
"usbErrorAlreadyActive": "A USB connection is already active.",
|
||||
"usbErrorNoDeviceSelected": "No USB device was selected.",
|
||||
"usbErrorPortClosed": "The USB connection is not open.",
|
||||
"usbErrorConnectTimedOut": "Connection timed out. Make sure the device has USB Companion firmware.",
|
||||
"usbFallbackDeviceName": "Web Serial Device",
|
||||
"usbStatus_notConnected": "Select a USB device",
|
||||
"usbStatus_connecting": "Connecting to USB device...",
|
||||
"usbStatus_searching": "Searching for USB devices...",
|
||||
"usbConnectionFailed": "USB connection failed: {error}",
|
||||
"@usbConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner_scanning": "Scanning for devices...",
|
||||
"scanner_connecting": "Connecting...",
|
||||
"scanner_disconnecting": "Disconnecting...",
|
||||
@@ -131,8 +72,6 @@
|
||||
"scanner_scan": "Scan",
|
||||
"scanner_bluetoothOff": "Bluetooth is off",
|
||||
"scanner_bluetoothOffMessage": "Please turn on Bluetooth to scan for devices",
|
||||
"scanner_chromeRequired": "Chrome Browser Required",
|
||||
"scanner_chromeRequiredMessage": "This web application requires Google Chrome or a Chromium-based browser for Bluetooth support.",
|
||||
"scanner_enableBluetooth": "Enable Bluetooth",
|
||||
"device_quickSwitch": "Quick switch",
|
||||
"device_meshcore": "MeshCore",
|
||||
@@ -159,33 +98,11 @@
|
||||
"settings_locationIntervalInvalid": "Interval must be at least 60 seconds, and less than 86400 seconds.",
|
||||
"settings_latitude": "Latitude",
|
||||
"settings_longitude": "Longitude",
|
||||
"settings_contactSettings": "Contact Settings",
|
||||
"settings_contactSettingsSubtitle": "Settings for how contacts are added.",
|
||||
"settings_privacyMode": "Privacy Mode",
|
||||
"settings_privacyModeSubtitle": "Hide name/location in advertisements",
|
||||
"settings_privacyModeToggle": "Toggle privacy mode to hide your name and location in advertisements.",
|
||||
"settings_privacyModeEnabled": "Privacy mode enabled",
|
||||
"settings_privacyModeDisabled": "Privacy mode disabled",
|
||||
"settings_privacy": "Privacy Settings",
|
||||
"settings_privacySubtitle": "Control what information is shared.",
|
||||
"settings_privacySettingsDescription": "Choose what information your device shares with others.",
|
||||
"settings_denyAll": "Deny all",
|
||||
"settings_allowByContact": "Allow by contact flags",
|
||||
"settings_allowAll": "Allow all",
|
||||
"settings_telemetryBaseMode": "Telemetry Base Mode",
|
||||
"settings_telemetryLocationMode": "Telemetry Location Mode",
|
||||
"settings_telemetryEnvironmentMode": "Telemetry Environment Mode",
|
||||
"settings_advertLocation": "Advert Location",
|
||||
"settings_advertLocationSubtitle": "Include location in advert.",
|
||||
"settings_multiAck": "Multi-ACKs: {value}",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_telemetryModeUpdated": "Telemetry mode updated",
|
||||
"settings_actions": "Actions",
|
||||
"settings_sendAdvertisement": "Send Advertisement",
|
||||
"settings_sendAdvertisementSubtitle": "Broadcast presence now",
|
||||
@@ -436,7 +353,6 @@
|
||||
"contacts_newGroup": "New Group",
|
||||
"contacts_groupName": "Group name",
|
||||
"contacts_groupNameRequired": "Group name is required",
|
||||
"contacts_groupNameReserved": "This group name is reserved",
|
||||
"contacts_groupAlreadyExists": "Group \"{name}\" already exists",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -448,8 +364,8 @@
|
||||
"contacts_filterContacts": "Filter contacts...",
|
||||
"contacts_noContactsMatchFilter": "No contacts match your filter",
|
||||
"contacts_noMembers": "No members",
|
||||
"contacts_lastSeenNow": "recently",
|
||||
"contacts_lastSeenMinsAgo": "~ {minutes} min.",
|
||||
"contacts_lastSeenNow": "Last seen now",
|
||||
"contacts_lastSeenMinsAgo": "Last seen {minutes} mins ago",
|
||||
"@contacts_lastSeenMinsAgo": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
@@ -457,8 +373,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_lastSeenHourAgo": "~ 1 hour",
|
||||
"contacts_lastSeenHoursAgo": "~ {hours} hours",
|
||||
"contacts_lastSeenHourAgo": "Last seen 1 hour ago",
|
||||
"contacts_lastSeenHoursAgo": "Last seen {hours} hours ago",
|
||||
"@contacts_lastSeenHoursAgo": {
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
@@ -466,8 +382,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_lastSeenDayAgo": "~ 1 day",
|
||||
"contacts_lastSeenDaysAgo": "~ {days} days",
|
||||
"contacts_lastSeenDayAgo": "Last seen 1 day ago",
|
||||
"contacts_lastSeenDaysAgo": "Last seen {days} days ago",
|
||||
"@contacts_lastSeenDaysAgo": {
|
||||
"placeholders": {
|
||||
"days": {
|
||||
@@ -475,17 +391,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contact_info": "Contact Info",
|
||||
"contact_settings": "Contact Settings",
|
||||
"contact_telemetry": "Telemetry",
|
||||
"contact_lastSeen": "Last seen",
|
||||
"contact_clearChat": "Clear Chat",
|
||||
"contact_teleBase": "Telemetry Base",
|
||||
"contact_teleBaseSubtitle": "Allow sharing battery level and basic telemetry",
|
||||
"contact_teleLoc": "Telemetry Location",
|
||||
"contact_teleLocSubtitle": "Allow sharing location data",
|
||||
"contact_teleEnv": "Telemetry Environment",
|
||||
"contact_teleEnvSubtitle": "Allow sharing environment sensor data",
|
||||
"channels_title": "Channels",
|
||||
"channels_noChannelsConfigured": "No channels configured",
|
||||
"channels_addPublicChannel": "Add Public Channel",
|
||||
@@ -839,7 +744,6 @@
|
||||
"map_source": "Source",
|
||||
"map_flags": "Flags",
|
||||
"map_shareMarkerHere": "Share marker here",
|
||||
"map_setAsMyLocation": "Set as my location",
|
||||
"map_pinLabel": "Pin label",
|
||||
"map_label": "Label",
|
||||
"map_pointOfInterest": "Point of interest",
|
||||
@@ -866,9 +770,6 @@
|
||||
"map_publicKeyPrefix": "Public key prefix",
|
||||
"map_markers": "Markers",
|
||||
"map_showSharedMarkers": "Show shared markers",
|
||||
"map_showGuessedLocations": "Show guessed node locations",
|
||||
"map_showDiscoveryContacts": "Show Discovery Contacts",
|
||||
"map_guessedLocation": "Guessed location",
|
||||
"map_lastSeenTime": "Last Seen Time",
|
||||
"map_sharedPin": "Shared pin",
|
||||
"map_joinRoom": "Join Room",
|
||||
@@ -1936,27 +1837,5 @@
|
||||
"settings_gpxExportShareText": "Map data exported from meshcore-open",
|
||||
"settings_gpxExportShareSubject": "meshcore-open GPX map data export",
|
||||
"snrIndicator_nearByRepeaters": "Nearby Repeaters",
|
||||
"snrIndicator_lastSeen": "Last seen",
|
||||
"contactsSettings_title": "Contacts settings",
|
||||
"contactsSettings_autoAddTitle": "Automatic Discovery",
|
||||
"contactsSettings_otherTitle": "Other contact related settings",
|
||||
"contactsSettings_autoAddUsersTitle": "Auto-add users",
|
||||
"contactsSettings_autoAddUsersSubtitle": "Allow the companion to automatically add discovered users.",
|
||||
"contactsSettings_autoAddRepeatersTitle": "Auto-add repeaters",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "Allow the companion to automatically add discovered repeaters.",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Auto-add room servers",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Allow the companion to automatically add discovered room servers.",
|
||||
"contactsSettings_autoAddSensorsTitle": "Auto-add sensors",
|
||||
"contactsSettings_autoAddSensorsSubtitle": "Allow the companion to automatically add discovered sensors.",
|
||||
"contactsSettings_overwriteOldestTitle": "Overwrite Oldest",
|
||||
"contactsSettings_overwriteOldestSubtitle": "When the contact list is full, the oldest non-favorited contact will be replaced.",
|
||||
"discoveredContacts_Title": "Discovered Contacts",
|
||||
"discoveredContacts_noMatching": "No matching contacts",
|
||||
"discoveredContacts_searchHint": "Search discovered contacts",
|
||||
"discoveredContacts_contactAdded": "Contact added",
|
||||
"discoveredContacts_addContact": "Add Contact",
|
||||
"discoveredContacts_copyContact": "Copy Contact to clipboard",
|
||||
"discoveredContacts_deleteContact": "Delete Discovered Contact",
|
||||
"discoveredContacts_deleteContactAll": "Delete All Discovered Contacts",
|
||||
"discoveredContacts_deleteContactAllContent": "Are you sure you want to delete all discovered contacts?"
|
||||
}
|
||||
"snrIndicator_lastSeen": "Last seen"
|
||||
}
|
||||
|
||||
+6
-127
@@ -285,7 +285,6 @@
|
||||
"contacts_newGroup": "Nuevo Grupo",
|
||||
"contacts_groupName": "Nombre del grupo",
|
||||
"contacts_groupNameRequired": "El nombre del grupo es obligatorio",
|
||||
"contacts_groupNameReserved": "Este nombre de grupo está reservado",
|
||||
"contacts_groupAlreadyExists": "El grupo \"{name}\" ya existe",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -298,7 +297,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": {
|
||||
@@ -306,8 +305,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": {
|
||||
@@ -315,8 +314,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": {
|
||||
@@ -1633,8 +1632,6 @@
|
||||
"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",
|
||||
@@ -1830,123 +1827,5 @@
|
||||
"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",
|
||||
"settings_privacySubtitle": "Controlar qué información se comparte.",
|
||||
"settings_allowByContact": "Permitir por banderas de contacto",
|
||||
"settings_denyAll": "Denegar todo",
|
||||
"settings_telemetryBaseMode": "Modo base de telemetría",
|
||||
"settings_telemetryEnvironmentMode": "Modo de entorno de telemetría",
|
||||
"settings_advertLocationSubtitle": "Incluir ubicación en anuncio",
|
||||
"contact_info": "Información de contacto",
|
||||
"settings_privacySettingsDescription": "Elige qué información comparte tu dispositivo con otros.",
|
||||
"settings_allowAll": "Permitir todo",
|
||||
"settings_privacy": "Configuración de privacidad",
|
||||
"contact_settings": "Configuración de contacto",
|
||||
"settings_telemetryLocationMode": "Modo de ubicación de telemetría",
|
||||
"contact_teleBase": "Base de Telemetría",
|
||||
"contact_teleLoc": "Ubicación de telemetría",
|
||||
"settings_advertLocation": "Ubicación de anuncio",
|
||||
"contact_teleLocSubtitle": "Permitir el intercambio de datos de ubicación",
|
||||
"contact_clearChat": "Borrar chat",
|
||||
"contact_telemetry": "Telemetría",
|
||||
"contact_lastSeen": "Visto por última vez",
|
||||
"contact_teleBaseSubtitle": "Permitir el intercambio de nivel de batería y telemetría básica",
|
||||
"contact_teleEnv": "Entorno de Telemetría",
|
||||
"contact_teleEnvSubtitle": "Permitir el intercambio de datos de sensores de entorno",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_telemetryModeUpdated": "Modo de telemetría actualizado",
|
||||
"settings_multiAck": "Multi-ACKs: {value}"
|
||||
"contacts_searchRoomServers": "Buscar {number}{str} servidores de sala..."
|
||||
}
|
||||
|
||||
+6
-127
@@ -285,7 +285,6 @@
|
||||
"contacts_newGroup": "Nouveau Groupe",
|
||||
"contacts_groupName": "Nom du groupe",
|
||||
"contacts_groupNameRequired": "Le nom du groupe est obligatoire.",
|
||||
"contacts_groupNameReserved": "Ce nom de groupe est réservé",
|
||||
"contacts_groupAlreadyExists": "Le groupe \"{name}\" existe déjà.",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -298,7 +297,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": {
|
||||
@@ -306,8 +305,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": {
|
||||
@@ -315,8 +314,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": {
|
||||
@@ -1605,8 +1604,6 @@
|
||||
"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",
|
||||
@@ -1802,123 +1799,5 @@
|
||||
"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",
|
||||
"settings_privacy": "Paramètres de confidentialité",
|
||||
"settings_privacySubtitle": "Contrôlez les informations partagées",
|
||||
"settings_telemetryLocationMode": "Mode d'emplacement de télémétrie",
|
||||
"settings_telemetryEnvironmentMode": "Mode d'environnement de télémétrie",
|
||||
"settings_advertLocation": "Emplacement de l'annonce",
|
||||
"settings_advertLocationSubtitle": "Inclure l'emplacement dans l'annonce",
|
||||
"settings_denyAll": "Refuser tout",
|
||||
"settings_allowByContact": "Autoriser par drapeaux de contact",
|
||||
"settings_privacySettingsDescription": "Choisissez les informations que votre appareil partage avec les autres.",
|
||||
"settings_allowAll": "Autoriser tout",
|
||||
"contact_info": "Informations de contact",
|
||||
"settings_telemetryBaseMode": "Mode de base Télémétrie",
|
||||
"contact_teleBase": "Base de télémétrie",
|
||||
"contact_teleLoc": "Emplacement de télémétrie",
|
||||
"contact_teleLocSubtitle": "Autoriser le partage des données de localisation",
|
||||
"contact_teleEnv": "Environnement Télémétrie",
|
||||
"contact_teleEnvSubtitle": "Autoriser le partage des données des capteurs d'environnement",
|
||||
"contact_telemetry": "Télémétrie",
|
||||
"contact_settings": "Paramètres de contact",
|
||||
"contact_lastSeen": "Dernière fois vu",
|
||||
"contact_clearChat": "Effacer la conversation",
|
||||
"contact_teleBaseSubtitle": "Autoriser le partage du niveau de batterie et de la télémétrie de base",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_multiAck": "Multi-ACKs : {value}",
|
||||
"settings_telemetryModeUpdated": "Le mode télémétrie a été mis à jour"
|
||||
"contacts_searchContactsNoNumber": "Rechercher des contacts..."
|
||||
}
|
||||
|
||||
+1
-122
@@ -285,7 +285,6 @@
|
||||
"contacts_newGroup": "Nuovo Gruppo",
|
||||
"contacts_groupName": "Nome gruppo",
|
||||
"contacts_groupNameRequired": "Il nome del gruppo è obbligatorio.",
|
||||
"contacts_groupNameReserved": "Questo nome del gruppo è riservato",
|
||||
"contacts_groupAlreadyExists": "Il gruppo \"{name}\" esiste già.",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -1606,8 +1605,6 @@
|
||||
"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",
|
||||
@@ -1802,123 +1799,5 @@
|
||||
"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",
|
||||
"settings_privacySettingsDescription": "Scegli le informazioni che il tuo dispositivo condivide con gli altri.",
|
||||
"settings_allowByContact": "Consenti in base ai flag di contatto",
|
||||
"settings_telemetryLocationMode": "Modalità di posizionamento telemetrico",
|
||||
"settings_telemetryEnvironmentMode": "Modalità di ambiente di telemetria",
|
||||
"settings_advertLocation": "Posizione dell'annuncio",
|
||||
"settings_advertLocationSubtitle": "Includi la posizione nell'annuncio",
|
||||
"settings_privacy": "Impostazioni sulla privacy",
|
||||
"settings_denyAll": "Negare tutto",
|
||||
"settings_privacySubtitle": "Controlla le informazioni che vengono condivise.",
|
||||
"settings_allowAll": "Consenti tutto",
|
||||
"contact_info": "Informazioni di Contatto",
|
||||
"settings_telemetryBaseMode": "Modalità di base di telemetria",
|
||||
"contact_teleBase": "Base di telemetria",
|
||||
"contact_teleLoc": "Posizione telemetria",
|
||||
"contact_teleLocSubtitle": "Consenti la condivisione dei dati di posizione",
|
||||
"contact_clearChat": "Cancella chat",
|
||||
"contact_telemetry": "Telemetria",
|
||||
"contact_settings": "Impostazioni di contatto",
|
||||
"contact_lastSeen": "Ultimo accesso",
|
||||
"contact_teleBaseSubtitle": "Consenti la condivisione del livello della batteria e della telemetria di base",
|
||||
"contact_teleEnvSubtitle": "Consenti la condivisione dei dati del sensore ambientale",
|
||||
"contact_teleEnv": "Ambiente di telemetria",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_telemetryModeUpdated": "Modalità telemetria aggiornata",
|
||||
"settings_multiAck": "Multi-ACKs: {value}"
|
||||
"contacts_searchRoomServers": "Cerca {number}{str} server Room..."
|
||||
}
|
||||
|
||||
@@ -184,12 +184,6 @@ abstract class AppLocalizations {
|
||||
/// **'Delete'**
|
||||
String get common_delete;
|
||||
|
||||
/// No description provided for @common_deleteAll.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete All'**
|
||||
String get common_deleteAll;
|
||||
|
||||
/// No description provided for @common_close.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -322,228 +316,6 @@ abstract class AppLocalizations {
|
||||
/// **'MeshCore Open'**
|
||||
String get scanner_title;
|
||||
|
||||
/// No description provided for @connectionChoiceUsbLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'USB'**
|
||||
String get connectionChoiceUsbLabel;
|
||||
|
||||
/// No description provided for @connectionChoiceBluetoothLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Bluetooth'**
|
||||
String get connectionChoiceBluetoothLabel;
|
||||
|
||||
/// No description provided for @connectionChoiceTcpLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'TCP'**
|
||||
String get connectionChoiceTcpLabel;
|
||||
|
||||
/// No description provided for @tcpScreenTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Connect over TCP'**
|
||||
String get tcpScreenTitle;
|
||||
|
||||
/// No description provided for @tcpHostLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'IP Address'**
|
||||
String get tcpHostLabel;
|
||||
|
||||
/// No description provided for @tcpHostHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'192.168.40.10'**
|
||||
String get tcpHostHint;
|
||||
|
||||
/// No description provided for @tcpPortLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Port'**
|
||||
String get tcpPortLabel;
|
||||
|
||||
/// No description provided for @tcpPortHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'5000'**
|
||||
String get tcpPortHint;
|
||||
|
||||
/// No description provided for @tcpStatus_notConnected.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter endpoint and connect'**
|
||||
String get tcpStatus_notConnected;
|
||||
|
||||
/// No description provided for @tcpStatus_connectingTo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Connecting to {endpoint}...'**
|
||||
String tcpStatus_connectingTo(String endpoint);
|
||||
|
||||
/// No description provided for @tcpErrorHostRequired.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'IP address is required.'**
|
||||
String get tcpErrorHostRequired;
|
||||
|
||||
/// No description provided for @tcpErrorPortInvalid.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Port must be between 1 and 65535.'**
|
||||
String get tcpErrorPortInvalid;
|
||||
|
||||
/// No description provided for @tcpErrorUnsupported.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'TCP transport is not supported on this platform.'**
|
||||
String get tcpErrorUnsupported;
|
||||
|
||||
/// No description provided for @tcpErrorTimedOut.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'TCP connection timed out.'**
|
||||
String get tcpErrorTimedOut;
|
||||
|
||||
/// No description provided for @tcpConnectionFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'TCP connection failed: {error}'**
|
||||
String tcpConnectionFailed(String error);
|
||||
|
||||
/// No description provided for @usbScreenTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Connect over USB'**
|
||||
String get usbScreenTitle;
|
||||
|
||||
/// No description provided for @usbScreenSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Choose a detected serial device and connect directly to your MeshCore node.'**
|
||||
String get usbScreenSubtitle;
|
||||
|
||||
/// No description provided for @usbScreenStatus.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select a USB device'**
|
||||
String get usbScreenStatus;
|
||||
|
||||
/// No description provided for @usbScreenNote.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'USB serial is active on supported Android devices and desktop platforms.'**
|
||||
String get usbScreenNote;
|
||||
|
||||
/// No description provided for @usbScreenEmptyState.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No USB devices found. Plug one in and refresh.'**
|
||||
String get usbScreenEmptyState;
|
||||
|
||||
/// No description provided for @usbErrorPermissionDenied.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'USB permission was denied.'**
|
||||
String get usbErrorPermissionDenied;
|
||||
|
||||
/// No description provided for @usbErrorDeviceMissing.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The selected USB device is no longer available.'**
|
||||
String get usbErrorDeviceMissing;
|
||||
|
||||
/// No description provided for @usbErrorInvalidPort.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select a valid USB device.'**
|
||||
String get usbErrorInvalidPort;
|
||||
|
||||
/// No description provided for @usbErrorBusy.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Another USB connection request is already in progress.'**
|
||||
String get usbErrorBusy;
|
||||
|
||||
/// No description provided for @usbErrorNotConnected.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No USB device is connected.'**
|
||||
String get usbErrorNotConnected;
|
||||
|
||||
/// No description provided for @usbErrorOpenFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Failed to open the selected USB device.'**
|
||||
String get usbErrorOpenFailed;
|
||||
|
||||
/// No description provided for @usbErrorConnectFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Failed to connect to the selected USB device.'**
|
||||
String get usbErrorConnectFailed;
|
||||
|
||||
/// No description provided for @usbErrorUnsupported.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'USB serial is not supported on this platform.'**
|
||||
String get usbErrorUnsupported;
|
||||
|
||||
/// No description provided for @usbErrorAlreadyActive.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'A USB connection is already active.'**
|
||||
String get usbErrorAlreadyActive;
|
||||
|
||||
/// No description provided for @usbErrorNoDeviceSelected.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No USB device was selected.'**
|
||||
String get usbErrorNoDeviceSelected;
|
||||
|
||||
/// No description provided for @usbErrorPortClosed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The USB connection is not open.'**
|
||||
String get usbErrorPortClosed;
|
||||
|
||||
/// No description provided for @usbErrorConnectTimedOut.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Connection timed out. Make sure the device has USB Companion firmware.'**
|
||||
String get usbErrorConnectTimedOut;
|
||||
|
||||
/// No description provided for @usbFallbackDeviceName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Web Serial Device'**
|
||||
String get usbFallbackDeviceName;
|
||||
|
||||
/// No description provided for @usbStatus_notConnected.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select a USB device'**
|
||||
String get usbStatus_notConnected;
|
||||
|
||||
/// No description provided for @usbStatus_connecting.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Connecting to USB device...'**
|
||||
String get usbStatus_connecting;
|
||||
|
||||
/// No description provided for @usbStatus_searching.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Searching for USB devices...'**
|
||||
String get usbStatus_searching;
|
||||
|
||||
/// No description provided for @usbConnectionFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'USB connection failed: {error}'**
|
||||
String usbConnectionFailed(String error);
|
||||
|
||||
/// No description provided for @scanner_scanning.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -616,18 +388,6 @@ abstract class AppLocalizations {
|
||||
/// **'Please turn on Bluetooth to scan for devices'**
|
||||
String get scanner_bluetoothOffMessage;
|
||||
|
||||
/// No description provided for @scanner_chromeRequired.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Chrome Browser Required'**
|
||||
String get scanner_chromeRequired;
|
||||
|
||||
/// No description provided for @scanner_chromeRequiredMessage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This web application requires Google Chrome or a Chromium-based browser for Bluetooth support.'**
|
||||
String get scanner_chromeRequiredMessage;
|
||||
|
||||
/// No description provided for @scanner_enableBluetooth.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -784,18 +544,6 @@ abstract class AppLocalizations {
|
||||
/// **'Longitude'**
|
||||
String get settings_longitude;
|
||||
|
||||
/// No description provided for @settings_contactSettings.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Contact Settings'**
|
||||
String get settings_contactSettings;
|
||||
|
||||
/// No description provided for @settings_contactSettingsSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Settings for how contacts are added.'**
|
||||
String get settings_contactSettingsSubtitle;
|
||||
|
||||
/// No description provided for @settings_privacyMode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -826,84 +574,6 @@ abstract class AppLocalizations {
|
||||
/// **'Privacy mode disabled'**
|
||||
String get settings_privacyModeDisabled;
|
||||
|
||||
/// No description provided for @settings_privacy.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Privacy Settings'**
|
||||
String get settings_privacy;
|
||||
|
||||
/// No description provided for @settings_privacySubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Control what information is shared.'**
|
||||
String get settings_privacySubtitle;
|
||||
|
||||
/// No description provided for @settings_privacySettingsDescription.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Choose what information your device shares with others.'**
|
||||
String get settings_privacySettingsDescription;
|
||||
|
||||
/// No description provided for @settings_denyAll.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Deny all'**
|
||||
String get settings_denyAll;
|
||||
|
||||
/// No description provided for @settings_allowByContact.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Allow by contact flags'**
|
||||
String get settings_allowByContact;
|
||||
|
||||
/// No description provided for @settings_allowAll.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Allow all'**
|
||||
String get settings_allowAll;
|
||||
|
||||
/// No description provided for @settings_telemetryBaseMode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Telemetry Base Mode'**
|
||||
String get settings_telemetryBaseMode;
|
||||
|
||||
/// No description provided for @settings_telemetryLocationMode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Telemetry Location Mode'**
|
||||
String get settings_telemetryLocationMode;
|
||||
|
||||
/// No description provided for @settings_telemetryEnvironmentMode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Telemetry Environment Mode'**
|
||||
String get settings_telemetryEnvironmentMode;
|
||||
|
||||
/// No description provided for @settings_advertLocation.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Advert Location'**
|
||||
String get settings_advertLocation;
|
||||
|
||||
/// No description provided for @settings_advertLocationSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Include location in advert.'**
|
||||
String get settings_advertLocationSubtitle;
|
||||
|
||||
/// No description provided for @settings_multiAck.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Multi-ACKs: {value}'**
|
||||
String settings_multiAck(String value);
|
||||
|
||||
/// No description provided for @settings_telemetryModeUpdated.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Telemetry mode updated'**
|
||||
String get settings_telemetryModeUpdated;
|
||||
|
||||
/// No description provided for @settings_actions.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1792,12 +1462,6 @@ abstract class AppLocalizations {
|
||||
/// **'Group name is required'**
|
||||
String get contacts_groupNameRequired;
|
||||
|
||||
/// No description provided for @contacts_groupNameReserved.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This group name is reserved'**
|
||||
String get contacts_groupNameReserved;
|
||||
|
||||
/// No description provided for @contacts_groupAlreadyExists.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1825,105 +1489,39 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @contacts_lastSeenNow.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'recently'**
|
||||
/// **'Last seen now'**
|
||||
String get contacts_lastSeenNow;
|
||||
|
||||
/// No description provided for @contacts_lastSeenMinsAgo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'~ {minutes} min.'**
|
||||
/// **'Last seen {minutes} mins ago'**
|
||||
String contacts_lastSeenMinsAgo(int minutes);
|
||||
|
||||
/// No description provided for @contacts_lastSeenHourAgo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'~ 1 hour'**
|
||||
/// **'Last seen 1 hour ago'**
|
||||
String get contacts_lastSeenHourAgo;
|
||||
|
||||
/// No description provided for @contacts_lastSeenHoursAgo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'~ {hours} hours'**
|
||||
/// **'Last seen {hours} hours ago'**
|
||||
String contacts_lastSeenHoursAgo(int hours);
|
||||
|
||||
/// No description provided for @contacts_lastSeenDayAgo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'~ 1 day'**
|
||||
/// **'Last seen 1 day ago'**
|
||||
String get contacts_lastSeenDayAgo;
|
||||
|
||||
/// No description provided for @contacts_lastSeenDaysAgo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'~ {days} days'**
|
||||
/// **'Last seen {days} days ago'**
|
||||
String contacts_lastSeenDaysAgo(int days);
|
||||
|
||||
/// No description provided for @contact_info.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Contact Info'**
|
||||
String get contact_info;
|
||||
|
||||
/// No description provided for @contact_settings.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Contact Settings'**
|
||||
String get contact_settings;
|
||||
|
||||
/// No description provided for @contact_telemetry.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Telemetry'**
|
||||
String get contact_telemetry;
|
||||
|
||||
/// No description provided for @contact_lastSeen.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Last seen'**
|
||||
String get contact_lastSeen;
|
||||
|
||||
/// No description provided for @contact_clearChat.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Clear Chat'**
|
||||
String get contact_clearChat;
|
||||
|
||||
/// No description provided for @contact_teleBase.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Telemetry Base'**
|
||||
String get contact_teleBase;
|
||||
|
||||
/// No description provided for @contact_teleBaseSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Allow sharing battery level and basic telemetry'**
|
||||
String get contact_teleBaseSubtitle;
|
||||
|
||||
/// No description provided for @contact_teleLoc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Telemetry Location'**
|
||||
String get contact_teleLoc;
|
||||
|
||||
/// No description provided for @contact_teleLocSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Allow sharing location data'**
|
||||
String get contact_teleLocSubtitle;
|
||||
|
||||
/// No description provided for @contact_teleEnv.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Telemetry Environment'**
|
||||
String get contact_teleEnv;
|
||||
|
||||
/// No description provided for @contact_teleEnvSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Allow sharing environment sensor data'**
|
||||
String get contact_teleEnvSubtitle;
|
||||
|
||||
/// No description provided for @channels_title.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -2896,12 +2494,6 @@ abstract class AppLocalizations {
|
||||
/// **'Share marker here'**
|
||||
String get map_shareMarkerHere;
|
||||
|
||||
/// No description provided for @map_setAsMyLocation.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Set as my location'**
|
||||
String get map_setAsMyLocation;
|
||||
|
||||
/// No description provided for @map_pinLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -3016,24 +2608,6 @@ abstract class AppLocalizations {
|
||||
/// **'Show shared markers'**
|
||||
String get map_showSharedMarkers;
|
||||
|
||||
/// No description provided for @map_showGuessedLocations.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Show guessed node locations'**
|
||||
String get map_showGuessedLocations;
|
||||
|
||||
/// No description provided for @map_showDiscoveryContacts.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Show Discovery Contacts'**
|
||||
String get map_showDiscoveryContacts;
|
||||
|
||||
/// No description provided for @map_guessedLocation.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Guessed location'**
|
||||
String get map_guessedLocation;
|
||||
|
||||
/// No description provided for @map_lastSeenTime.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -5806,138 +5380,6 @@ abstract class AppLocalizations {
|
||||
/// In en, this message translates to:
|
||||
/// **'Last seen'**
|
||||
String get snrIndicator_lastSeen;
|
||||
|
||||
/// No description provided for @contactsSettings_title.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Contacts settings'**
|
||||
String get contactsSettings_title;
|
||||
|
||||
/// No description provided for @contactsSettings_autoAddTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Automatic Discovery'**
|
||||
String get contactsSettings_autoAddTitle;
|
||||
|
||||
/// No description provided for @contactsSettings_otherTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Other contact related settings'**
|
||||
String get contactsSettings_otherTitle;
|
||||
|
||||
/// No description provided for @contactsSettings_autoAddUsersTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Auto-add users'**
|
||||
String get contactsSettings_autoAddUsersTitle;
|
||||
|
||||
/// No description provided for @contactsSettings_autoAddUsersSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Allow the companion to automatically add discovered users.'**
|
||||
String get contactsSettings_autoAddUsersSubtitle;
|
||||
|
||||
/// No description provided for @contactsSettings_autoAddRepeatersTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Auto-add repeaters'**
|
||||
String get contactsSettings_autoAddRepeatersTitle;
|
||||
|
||||
/// No description provided for @contactsSettings_autoAddRepeatersSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Allow the companion to automatically add discovered repeaters.'**
|
||||
String get contactsSettings_autoAddRepeatersSubtitle;
|
||||
|
||||
/// No description provided for @contactsSettings_autoAddRoomServersTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Auto-add room servers'**
|
||||
String get contactsSettings_autoAddRoomServersTitle;
|
||||
|
||||
/// No description provided for @contactsSettings_autoAddRoomServersSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Allow the companion to automatically add discovered room servers.'**
|
||||
String get contactsSettings_autoAddRoomServersSubtitle;
|
||||
|
||||
/// No description provided for @contactsSettings_autoAddSensorsTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Auto-add sensors'**
|
||||
String get contactsSettings_autoAddSensorsTitle;
|
||||
|
||||
/// No description provided for @contactsSettings_autoAddSensorsSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Allow the companion to automatically add discovered sensors.'**
|
||||
String get contactsSettings_autoAddSensorsSubtitle;
|
||||
|
||||
/// No description provided for @contactsSettings_overwriteOldestTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Overwrite Oldest'**
|
||||
String get contactsSettings_overwriteOldestTitle;
|
||||
|
||||
/// No description provided for @contactsSettings_overwriteOldestSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'When the contact list is full, the oldest non-favorited contact will be replaced.'**
|
||||
String get contactsSettings_overwriteOldestSubtitle;
|
||||
|
||||
/// No description provided for @discoveredContacts_Title.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Discovered Contacts'**
|
||||
String get discoveredContacts_Title;
|
||||
|
||||
/// No description provided for @discoveredContacts_noMatching.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No matching contacts'**
|
||||
String get discoveredContacts_noMatching;
|
||||
|
||||
/// No description provided for @discoveredContacts_searchHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Search discovered contacts'**
|
||||
String get discoveredContacts_searchHint;
|
||||
|
||||
/// No description provided for @discoveredContacts_contactAdded.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Contact added'**
|
||||
String get discoveredContacts_contactAdded;
|
||||
|
||||
/// No description provided for @discoveredContacts_addContact.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add Contact'**
|
||||
String get discoveredContacts_addContact;
|
||||
|
||||
/// No description provided for @discoveredContacts_copyContact.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Copy Contact to clipboard'**
|
||||
String get discoveredContacts_copyContact;
|
||||
|
||||
/// No description provided for @discoveredContacts_deleteContact.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete Discovered Contact'**
|
||||
String get discoveredContacts_deleteContact;
|
||||
|
||||
/// No description provided for @discoveredContacts_deleteContactAll.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete All Discovered Contacts'**
|
||||
String get discoveredContacts_deleteContactAll;
|
||||
|
||||
/// No description provided for @discoveredContacts_deleteContactAllContent.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Are you sure you want to delete all discovered contacts?'**
|
||||
String get discoveredContacts_deleteContactAllContent;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -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 => 'Сканиране за устройства...';
|
||||
|
||||
@@ -281,13 +150,6 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Моля, активирайте Bluetooth, за да сканирате за устройства.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Изисква се браузър Chrome';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Активирайте Bluetooth';
|
||||
|
||||
@@ -372,13 +234,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 => 'Режим на поверителност';
|
||||
|
||||
@@ -398,52 +253,6 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
String get settings_privacyModeDisabled =>
|
||||
'Режим на поверителност е деактивиран';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Настройки на поверителността';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle =>
|
||||
'Контролирайте каква информация се споделя.';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Изберете каква информация устройството ви споделя с другите.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Откажи всичко';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Позволи по флагове за контакт';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Позволи всичко';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Базов режим на телеметрия';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode =>
|
||||
'Режим на местоположение на телеметрията';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode =>
|
||||
'Режим на средата на телеметрията';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Място на обявата';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Включи местоположение в обявата';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Мулти-потвърди: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Режим на телеметрията е обновен';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Действия';
|
||||
|
||||
@@ -948,9 +757,6 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'Името на групата е задължително.';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => 'Това име на група е запазено';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'Групата \"$name\" вече съществува.';
|
||||
@@ -990,42 +796,6 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
return 'Последно видян $days дни преди.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Контактна информация';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Настройки за контакти';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Телеметрия';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Последно видян';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Изчисти чата';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Базата данни за телеметрия';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Позволи споделяне на ниво на батерията и основна телеметрия';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Местоположение на телеметрията';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle =>
|
||||
'Позволи споделяне на данни за местоположение';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Среда на телеметрия';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle =>
|
||||
'Позволи споделяне на данни от средносферните датчици';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Канали';
|
||||
|
||||
@@ -1596,9 +1366,6 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Споделете маркер тук';
|
||||
|
||||
@override
|
||||
String get map_setAsMyLocation => 'Задайте като моя местоположение';
|
||||
|
||||
@override
|
||||
String get map_pinLabel => 'Етикетиране на пин';
|
||||
|
||||
@@ -1659,16 +1426,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 => 'Последна видяна дата';
|
||||
|
||||
@@ -3355,82 +3112,4 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Последно видян';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Настройки на контактите';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Автоматично откриване';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Други настройки свързани с контакти';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Автоматично добавяне на потребители';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Позволи на спътника да добавя автоматично откритите потребители.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Автоматично добавяне на повтарящи се елементи';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Позволи на спътника да добавя автоматично откритите повтарящи се устройства.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Автоматично добавяне на сървъри на стаите';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Позволи на спътника да добавя автоматично откритите сървъри на стаите.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Автоматично добавяне на датчици';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Позволи на спътника да добавя автоматично откритите датчици.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle => 'Премахни най-старото';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'Когато списъкът с контакти е пълен, най-старият неключов контакт ще бъде заменен.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Открити контакти';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => 'Няма съвпадащи контакти';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Търсене на открити контакти';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Контакт добавен';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Добави контакт';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact => 'Копирай контакт в клипборда';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Изтрий контакт';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Изтриване на Всички Открити Контакти';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Сигурни ли сте, че искате да изтриете всички открити контакти?';
|
||||
}
|
||||
|
||||
@@ -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...';
|
||||
|
||||
@@ -284,13 +150,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Chrome Browser erforderlich';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Diese Webanwendung erfordert Google Chrome oder einen Chromium-basierten Browser für die Bluetooth-Unterstützung.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Bluetooth aktivieren';
|
||||
|
||||
@@ -374,13 +233,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';
|
||||
|
||||
@@ -398,50 +250,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get settings_privacyModeDisabled => 'Datenschutzmodus deaktiviert';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Datenschutzeinstellungen';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle =>
|
||||
'Steuern Sie die Informationen, die freigegeben werden.';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Wählen Sie die Informationen, die Ihr Gerät mit anderen teilt.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Alle ablehnen';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Zulassen durch Kontaktflaggen';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Alles zulassen';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Telemetrie-Basismodus';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode => 'Telemetrie-Ortsmodus';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode => 'Telemetrie-Umgebungsmodus';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Anzeigenort';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Ort in der Anzeige einbeziehen';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Mehrfach-Bestätigungen: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Telemetriemodus aktualisiert';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Aktionen';
|
||||
|
||||
@@ -946,9 +754,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'Der Gruppennamen ist erforderlich.';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => 'Dieser Gruppenname ist reserviert';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'Die Gruppe \"$name\" existiert bereits.';
|
||||
@@ -965,64 +770,29 @@ 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
|
||||
String get contact_info => 'Kontaktinformationen';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Kontakteinstellungen';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Telemetrie';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Zuletzt gesehen';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Chat löschen';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Telemetriebasis';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Erlauben des Freigebens des Batteriestands und der grundlegenden Telemetrie';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Telemetrieort';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle => 'Teilen von Standortdaten zulassen';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Telemetrieumgebung';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle =>
|
||||
'Teilen von Umgebungsensordaten zulassen';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Kanäle';
|
||||
|
||||
@@ -1595,9 +1365,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';
|
||||
|
||||
@@ -1658,16 +1425,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';
|
||||
|
||||
@@ -3364,84 +3121,4 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Zuletzt gesehen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Kontakteinstellungen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Automatische Erkennung';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Weitere Einstellungen zu Kontakten';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Automatische Hinzufügung von Benutzern';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Ermöglichen Sie dem Begleiter, automatisch entdeckte Benutzer hinzuzufügen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Automatisch Repeater hinzufügen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Ermöglichen Sie dem Begleiter, automatisch entdeckte Repeater hinzuzufügen.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Automatisch Raumservers hinzufügen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Ermöglichen Sie dem Begleiter, entdeckte Raumserver automatisch hinzuzufügen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Automatisch Sensoren hinzufügen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Ermöglichen Sie dem Begleiter, automatisch entdeckte Sensoren hinzuzufügen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle =>
|
||||
'Überschreiben des Ältesten';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'Wenn die Kontaktliste voll ist, wird der älteste nicht favorisierte Kontakt ersetzt.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Entdeckte Kontakte';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => 'Keine passenden Kontakte';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Entdeckte Kontakte suchen';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Kontakt hinzugefügt';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Kontakt hinzufügen';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact =>
|
||||
'Kontakt in die Zwischenablage kopieren';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Kontakt löschen';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Alle entdeckten Kontakte löschen';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Sind Sie sicher, dass Sie alle gefundenen Kontakte löschen möchten?';
|
||||
}
|
||||
|
||||
@@ -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...';
|
||||
|
||||
@@ -278,13 +149,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Please turn on Bluetooth to scan for devices';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Chrome Browser Required';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'This web application requires Google Chrome or a Chromium-based browser for Bluetooth support.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Enable Bluetooth';
|
||||
|
||||
@@ -368,13 +232,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';
|
||||
|
||||
@@ -392,48 +249,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get settings_privacyModeDisabled => 'Privacy mode disabled';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Privacy Settings';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle => 'Control what information is shared.';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Choose what information your device shares with others.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Deny all';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Allow by contact flags';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Allow all';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Telemetry Base Mode';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode => 'Telemetry Location Mode';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode => 'Telemetry Environment Mode';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Advert Location';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => 'Include location in advert.';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Multi-ACKs: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Telemetry mode updated';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Actions';
|
||||
|
||||
@@ -931,9 +746,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'Group name is required';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => 'This group name is reserved';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'Group \"$name\" already exists';
|
||||
@@ -949,63 +761,29 @@ 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
|
||||
String get contact_info => 'Contact Info';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Contact Settings';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Telemetry';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Last seen';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Clear Chat';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Telemetry Base';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Allow sharing battery level and basic telemetry';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Telemetry Location';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle => 'Allow sharing location data';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Telemetry Environment';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle => 'Allow sharing environment sensor data';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Channels';
|
||||
|
||||
@@ -1566,9 +1344,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';
|
||||
|
||||
@@ -1629,15 +1404,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';
|
||||
|
||||
@@ -3299,78 +3065,4 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Last seen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Contacts settings';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Automatic Discovery';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle => 'Other contact related settings';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle => 'Auto-add users';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Allow the companion to automatically add discovered users.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Allow the companion to automatically add discovered repeaters.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Auto-add room servers';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Allow the companion to automatically add discovered room servers.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Allow the companion to automatically add discovered sensors.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'When the contact list is full, the oldest non-favorited contact will be replaced.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Discovered Contacts';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => 'No matching contacts';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Search discovered contacts';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Contact added';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Add Contact';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact => 'Copy Contact to clipboard';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Delete Discovered Contact';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Delete All Discovered Contacts';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Are you sure you want to delete all discovered contacts?';
|
||||
}
|
||||
|
||||
@@ -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...';
|
||||
|
||||
@@ -282,13 +150,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Por favor, active el Bluetooth para escanear dispositivos.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Navegador Chrome requerido';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Habilitar Bluetooth';
|
||||
|
||||
@@ -372,13 +233,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';
|
||||
|
||||
@@ -396,51 +250,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get settings_privacyModeDisabled => 'Modo de privacidad desactivado';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Configuración de privacidad';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle =>
|
||||
'Controlar qué información se comparte.';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Elige qué información comparte tu dispositivo con otros.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Denegar todo';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Permitir por banderas de contacto';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Permitir todo';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Modo base de telemetría';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode =>
|
||||
'Modo de ubicación de telemetría';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode =>
|
||||
'Modo de entorno de telemetría';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Ubicación de anuncio';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => 'Incluir ubicación en anuncio';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Multi-ACKs: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Modo de telemetría actualizado';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Acciones';
|
||||
|
||||
@@ -946,10 +755,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'El nombre del grupo es obligatorio';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved =>
|
||||
'Este nombre de grupo está reservado';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'El grupo \"$name\" ya existe';
|
||||
@@ -970,61 +775,25 @@ 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
|
||||
String get contact_info => 'Información de contacto';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Configuración de contacto';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Telemetría';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Visto por última vez';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Borrar chat';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Base de Telemetría';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Permitir el intercambio de nivel de batería y telemetría básica';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Ubicación de telemetría';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle =>
|
||||
'Permitir el intercambio de datos de ubicación';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Entorno de Telemetría';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle =>
|
||||
'Permitir el intercambio de datos de sensores de entorno';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Canales';
|
||||
|
||||
@@ -1594,9 +1363,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';
|
||||
|
||||
@@ -1657,16 +1423,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';
|
||||
|
||||
@@ -3357,85 +3113,4 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Visto por última vez';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Configuración de contactos';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Detección automática';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Otras configuraciones relacionadas con el contacto';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Agregar usuarios automáticamente';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Permitir que el compañero agregue automáticamente a los usuarios descubiertos.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Agregar repetidores automáticamente';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Permitir que el compañero agregue automáticamente los repetidores descubiertos.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Agregar automáticamente servidores de sala';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Permitir que el compañero agregue automáticamente los servidores de salas descubiertos.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Agregar sensores automáticamente';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Permitir que el compañero agregue automáticamente los sensores descubiertos.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle =>
|
||||
'Sobreescribir el más antiguo';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'Cuando la lista de contactos esté llena, se reemplazará el contacto no favorito más antiguo.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Contactos descubiertos';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching =>
|
||||
'No se encontraron contactos coincidentes';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Buscar contactos descubiertos';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Contacto agregado';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Agregar contacto';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact =>
|
||||
'Copiar contacto al portapapeles';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Eliminar contacto';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Eliminar Todos los Contactos Descubiertos';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'¿Está seguro de que desea eliminar todos los contactos descubiertos!';
|
||||
}
|
||||
|
||||
@@ -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...';
|
||||
|
||||
@@ -284,13 +150,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Veuillez activer le Bluetooth pour rechercher des appareils.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Navigateur Chrome requis';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Cette application web nécessite Google Chrome ou un navigateur basé sur Chromium pour le support Bluetooth.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Activer le Bluetooth';
|
||||
|
||||
@@ -375,13 +234,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é';
|
||||
|
||||
@@ -400,52 +252,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get settings_privacyModeDisabled =>
|
||||
'Mode de confidentialité désactivé';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Paramètres de confidentialité';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle => 'Contrôlez les informations partagées';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Choisissez les informations que votre appareil partage avec les autres.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Refuser tout';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Autoriser par drapeaux de contact';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Autoriser tout';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Mode de base Télémétrie';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode =>
|
||||
'Mode d\'emplacement de télémétrie';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode =>
|
||||
'Mode d\'environnement de télémétrie';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Emplacement de l\'annonce';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Inclure l\'emplacement dans l\'annonce';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Multi-ACKs : $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated =>
|
||||
'Le mode télémétrie a été mis à jour';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Actions';
|
||||
|
||||
@@ -951,9 +757,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'Le nom du groupe est obligatoire.';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => 'Ce nom de groupe est réservé';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'Le groupe \"$name\" existe déjà.';
|
||||
@@ -974,61 +777,25 @@ 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
|
||||
String get contact_info => 'Informations de contact';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Paramètres de contact';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Télémétrie';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Dernière fois vu';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Effacer la conversation';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Base de télémétrie';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Autoriser le partage du niveau de batterie et de la télémétrie de base';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Emplacement de télémétrie';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle =>
|
||||
'Autoriser le partage des données de localisation';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Environnement Télémétrie';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle =>
|
||||
'Autoriser le partage des données des capteurs d\'environnement';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Canaux';
|
||||
|
||||
@@ -1603,9 +1370,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';
|
||||
|
||||
@@ -1666,16 +1430,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';
|
||||
|
||||
@@ -3381,84 +3135,4 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Dernière fois vu';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Paramètres des contacts';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Découverte automatique';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Autres paramètres liés aux contacts';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Ajouter automatiquement les utilisateurs';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Autoriser le compagnon à ajouter automatiquement les utilisateurs découverts';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Ajouter automatiquement les répéteurs';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Autoriser le compagnon à ajouter automatiquement les répéteurs découverts';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Ajouter automatiquement les 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 ?';
|
||||
}
|
||||
|
||||
@@ -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...';
|
||||
|
||||
@@ -284,13 +150,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Browser Chrome richiesto';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Questa applicazione web richiede Google Chrome o un browser basato su Chromium per il supporto Bluetooth.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Abilita il Bluetooth';
|
||||
|
||||
@@ -374,13 +233,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';
|
||||
|
||||
@@ -398,52 +250,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get settings_privacyModeDisabled => 'Modalità privacy disabilitata';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Impostazioni sulla privacy';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle =>
|
||||
'Controlla le informazioni che vengono condivise.';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Scegli le informazioni che il tuo dispositivo condivide con gli altri.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Negare tutto';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Consenti in base ai flag di contatto';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Consenti tutto';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Modalità di base di telemetria';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode =>
|
||||
'Modalità di posizionamento telemetrico';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode =>
|
||||
'Modalità di ambiente di telemetria';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Posizione dell\'annuncio';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Includi la posizione nell\'annuncio';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Multi-ACKs: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Modalità telemetria aggiornata';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Azioni';
|
||||
|
||||
@@ -947,9 +753,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'Il nome del gruppo è obbligatorio.';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => 'Questo nome del gruppo è riservato';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'Il gruppo \"$name\" esiste già.';
|
||||
@@ -989,42 +792,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
return 'Ultimo visto $days giorni fa';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Informazioni di Contatto';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Impostazioni di contatto';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Telemetria';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Ultimo accesso';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Cancella chat';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Base di telemetria';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Consenti la condivisione del livello della batteria e della telemetria di base';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Posizione telemetria';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle =>
|
||||
'Consenti la condivisione dei dati di posizione';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Ambiente di telemetria';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle =>
|
||||
'Consenti la condivisione dei dati del sensore ambientale';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Canali';
|
||||
|
||||
@@ -1595,9 +1362,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';
|
||||
|
||||
@@ -1658,15 +1422,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';
|
||||
|
||||
@@ -3361,83 +3116,4 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Ultimo accesso';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Impostazioni dei contatti';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Scoperta automatica';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Altre impostazioni relative ai contatti';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Aggiungere utenti automaticamente';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Consenti al compagno di aggiungere automaticamente gli utenti scoperti.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Aggiungere ripetitori automaticamente';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Consenti al compagno di aggiungere automaticamente i ripetitori scoperti.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Aggiungere automaticamente i server delle stanze';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Consenti al compagno di aggiungere automaticamente i server delle stanze scoperte.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Aggiungere automaticamente i sensori';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Consenti al compagno di aggiungere automaticamente i sensori scoperti';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle =>
|
||||
'Sostituisci il più vecchio';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'Quando l\'elenco dei contatti è pieno, il contatto più vecchio non tra i preferiti verrà sostituito.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Contatti scoperti';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => 'Nessun contatto corrispondente';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Cerca contatti scoperti';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Contatto aggiunto';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Aggiungi contatto';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact => 'Copia contatto negli appunti';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Elimina Contatto';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Eliminare tutti i contatti scoperti';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Sei sicuro di voler eliminare tutti i contatti scoperti?';
|
||||
}
|
||||
|
||||
@@ -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...';
|
||||
|
||||
@@ -280,13 +149,6 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Chrome-browser vereist';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Deze webapplicatie vereist Google Chrome of een op Chromium gebaseerde browser voor Bluetooth-ondersteuning.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Activeer Bluetooth';
|
||||
|
||||
@@ -371,13 +233,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';
|
||||
|
||||
@@ -395,50 +250,6 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_privacyModeDisabled => 'Privacy modus is uitgeschakeld';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Privacyinstellingen';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle =>
|
||||
'Beheer welke informatie wordt gedeeld';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Kies welke informatie uw apparaat deelt met anderen';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Weiger alles';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Toestaan op basis van contactvlaggen';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Alles toestaan';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Telemetrie-basismodus';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode => 'Telemetrie-locatiemodus';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode => 'Telemetrie-omgevingsmodus';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Advertentielocatie';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Locatie opnemen in advertentie';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Multi-ACKs: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Telemetrie-modus bijgewerkt';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Acties';
|
||||
|
||||
@@ -939,9 +750,6 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'De groepnaam is verplicht.';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => 'Deze groepsnaam is gereserveerd';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'De groep \"$name\" bestaat al.';
|
||||
@@ -981,40 +789,6 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
return 'Laast gezien $days dagen geleden';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Contactinformatie';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Contactinstellingen';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Telemetrie';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Laatst gezien';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Chat leegmaken';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Telemetrie_basis';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Sta delen van batterij niveau en basis telemetrie toe';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Telemetrielocatie';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle => 'Locatiegegevens delen toestaan';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Telemetrieomgeving';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle => 'Delen van omgevingsensordata toestaan';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Kanaal';
|
||||
|
||||
@@ -1583,9 +1357,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';
|
||||
|
||||
@@ -1646,16 +1417,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';
|
||||
|
||||
@@ -3342,82 +3103,4 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Laatst gezien';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Instellingen voor contacten';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Automatische detectie';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Andere instellingen voor contactgerelateerde zaken';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Gebruikers automatisch toevoegen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Sta toe dat de companion automatisch ontdekte gebruikers toevoegt';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Automatisch herhalingstoestellen toevoegen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Sta toe dat de companion automatisch ontdekte repeaters toevoegt';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Automatisch kamerservers toevoegen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Sta toe dat de companion automatisch ontdekte kamer servers toevoegt.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Automatisch sensoren toevoegen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Sta toe dat de companion automatisch ontdekte sensoren toevoegt';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle => 'Overschrijf Oudste';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'Wanneer de contactenlijst vol is, wordt de oudste niet-favoriete contactpersoon vervangen.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Ontdekte contacten';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => 'Geen overeenkomende contacten';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Ontdekte contacten zoeken';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Contact toegevoegd';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Contact toevoegen';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact => 'Kopieer contact naar klembord';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Contact verwijderen';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Verwijder alle ontdekte contacten';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Weet u zeker dat u alle ontdekte contacten wilt verwijderen?';
|
||||
}
|
||||
|
||||
@@ -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ń...';
|
||||
|
||||
@@ -285,13 +150,6 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
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';
|
||||
|
||||
@@ -377,13 +235,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';
|
||||
|
||||
@@ -401,52 +252,6 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_privacyModeDisabled => 'Tryb prywatności wyłączony';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Ustawienia prywatności';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle =>
|
||||
'Kontroluj jakie informacje są udostępniane.';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Wybierz jakie informacje urządzenie udostępni innym.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Odmów wszystkim';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Zezwalaj według flag kontaktowych';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Zezwalaj na wszystko';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Tryb podstawowy telemetrii';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode => 'Tryb położenia telemetrycznego';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode =>
|
||||
'Tryb środowiska telemetrycznego';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Lokalizacja reklamowa';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Uwzględnij lokalizację w ogłoszeniu';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Wiele potwierdzeń: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated =>
|
||||
'Tryb telemetryczny zaktualizowany';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Działania';
|
||||
|
||||
@@ -950,9 +755,6 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'Nazwa grupy jest wymagana';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => 'Ta nazwa grupy jest zastrzeżona';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'Grupa \"$name\" już istnieje';
|
||||
@@ -992,42 +794,6 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
return 'Ostatnie połączenie $days dni temu';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Informacje kontaktowe';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Ustawienia kontaktowe';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Telemetryka';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Ostatnio widziany';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Wyczyść czat';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Baza telemetryczna';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Pozwól na udostępnianie poziomu naładowania baterii i podstawowych danych telemetrycznych';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Lokalizacja telemetryczna';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle =>
|
||||
'Zezwalaj na udostępnianie danych lokalizacji';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Środowisko telemetryczne';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle =>
|
||||
'Zezwalaj na udostępnianie danych czujników środowiskowych';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Kanały';
|
||||
|
||||
@@ -1597,9 +1363,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ę';
|
||||
|
||||
@@ -1660,16 +1423,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';
|
||||
|
||||
@@ -3363,82 +3116,4 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
|
||||
@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?';
|
||||
}
|
||||
|
||||
@@ -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...';
|
||||
|
||||
@@ -283,13 +150,6 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Por favor, ative o Bluetooth para escanear por dispositivos.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Navegador Chrome necessário';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Esta aplicação web requer o Google Chrome ou um navegador baseado no Chromium para suporte de Bluetooth.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Ative o Bluetooth';
|
||||
|
||||
@@ -374,13 +234,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';
|
||||
|
||||
@@ -398,51 +251,6 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get settings_privacyModeDisabled => 'Modo de privacidade desativado';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Configurações de Privacidade';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle => 'Controle o que é compartilhado.';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Escolha quais informações o seu dispositivo compartilha com os outros.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Negar todos';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Permitir por bandeiras de contato';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Permitir todos';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Modo Base de Telemetria';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode =>
|
||||
'Modo de Localização de Telemetria';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode =>
|
||||
'Modo de Ambiente de Telemetria';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Localização do Anúncio';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Incluir localização no anúncio';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Multi-ACKs: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Modo de telemetria atualizado';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Ações';
|
||||
|
||||
@@ -948,9 +756,6 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'O nome do grupo é obrigatório.';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => 'Este nome de grupo está reservado';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'O grupo \"$name\" já existe';
|
||||
@@ -990,42 +795,6 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
return 'Última vez visto $days dias atrás';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Informações de Contato';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Configurações de Contato';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Telemetria';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Visto pela última vez';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Limpar Chat';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Base de Telemetria';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Permitir compartilhamento do nível da bateria e telemetria básica';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Localização de Telemetria';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle =>
|
||||
'Permitir compartilhamento de dados de localização';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Ambiente de Telemetria';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle =>
|
||||
'Permitir compartilhamento de dados do sensor de ambiente';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Canais';
|
||||
|
||||
@@ -1595,9 +1364,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';
|
||||
|
||||
@@ -1658,16 +1424,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';
|
||||
|
||||
@@ -3355,84 +3111,4 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Visto pela última vez';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Configurações de contatos';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Descoberta Automática';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Outras configurações relacionadas a contatos';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Adicionar usuários automaticamente';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Permitir que o companheiro adicione automaticamente os usuários descobertos.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Adicionar repetidores automaticamente';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Permitir que o companheiro adicione automaticamente os repetidores descobertos.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Adicionar automaticamente servidores de sala';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Permitir que o companheiro adicione automaticamente os servidores de salas descobertos.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Adicionar sensores automaticamente';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Permitir que o companheiro adicione automaticamente sensores descobertos.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle =>
|
||||
'Sobrescrever o Mais Antigo';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'Quando a lista de contatos estiver cheia, o contato mais antigo não favoritado será substituído.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Contatos Descobertos';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => 'Nenhum contato correspondente';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Pesquisar contatos descobertos';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Contato adicionado';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Adicionar Contato';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact =>
|
||||
'Copiar Contato para a área de transferência';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Excluir Contato';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Excluir Todos os Contatos Descobertos';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Tem certeza de que deseja excluir todos os contatos descobertos?';
|
||||
}
|
||||
|
||||
@@ -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 => 'Поиск устройств...';
|
||||
|
||||
@@ -283,13 +149,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Пожалуйста, включите Bluetooth, чтобы найти устройства.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Требуется браузер Chrome';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Включите Bluetooth';
|
||||
|
||||
@@ -373,13 +232,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 => 'Режим конфиденциальности';
|
||||
|
||||
@@ -398,51 +250,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get settings_privacyModeDisabled =>
|
||||
'Режим конфиденциальности выключен';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Настройки конфиденциальности';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle =>
|
||||
'Контролируйте, какую информацию делиться.';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Выберите, какую информацию ваше устройство будет делиться с другими.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Отклонить все';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Разрешить по флагам контактов';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Разрешить все';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Базовый режим телеметрии';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode =>
|
||||
'Режим местоположения телеметрии';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode => 'Режим среды телеметрии';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Местоположение рекламы';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Включить местоположение в объявление';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Мульти-ACK: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Режим телеметрии обновлен';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Действия';
|
||||
|
||||
@@ -947,9 +754,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'Имя группы обязательно';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => 'Это имя группы зарезервировано';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'Группа \"$name\" уже существует';
|
||||
@@ -989,42 +793,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
return 'Видели $days дн. назад';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Контактная информация';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Настройки контактов';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Телеметрия';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Последний раз видели';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Очистить чат';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'База телеметрии';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Разрешить обмен уровнем заряда батареи и базовой телеметрией';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Местоположение телеметрии';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle =>
|
||||
'Разрешить обмен данными о местоположении';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Среда телеметрии';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle =>
|
||||
'Разрешить обмен данными датчиков окружающей среды';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Каналы';
|
||||
|
||||
@@ -1597,9 +1365,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Поделиться меткой здесь';
|
||||
|
||||
@override
|
||||
String get map_setAsMyLocation => 'Установить мое местоположение';
|
||||
|
||||
@override
|
||||
String get map_pinLabel => 'Метка';
|
||||
|
||||
@@ -1660,16 +1425,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 => 'Время последнего появления';
|
||||
|
||||
@@ -3368,84 +3123,4 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Последний раз видели';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Настройки контактов';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Автоматическое обнаружение';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Другие настройки, связанные с контактами';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Автоматически добавлять пользователей';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Разрешить компаньону автоматически добавлять обнаруженных пользователей';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Автоматически добавлять ретрансляторы';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Разрешить спутнику автоматически добавлять обнаруженные ретрансляторы';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Автоматически добавлять серверы комнат';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Разрешить компаньону автоматически добавлять обнаруженные сервера комнат.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Автоматически добавлять датчики';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Разрешить компаньону автоматически добавлять обнаруженные датчики';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle =>
|
||||
'Перезаписать самое старое';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'Когда список контактов заполнен, будет заменен самый старый контакт, который не находится в избранном.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Обнаруженные контакты';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => 'Нет совпадающих контактов';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Найденные контакты поиска';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Контакт добавлен';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Добавить контакт';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact =>
|
||||
'Копировать контакт в буфер обмена';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Удалить контакт';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Удалить Все Обнаруженные Контакты';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Вы уверены, что хотите удалить все обнаруженные контакты?';
|
||||
}
|
||||
|
||||
@@ -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í...';
|
||||
|
||||
@@ -282,13 +150,6 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Vyžaduje sa prehliadač Chrome';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Táto webová aplikácia vyžaduje Google Chrome alebo prehliadač založený na Chromium pre podporu Bluetooth.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Povolte Bluetooth';
|
||||
|
||||
@@ -372,13 +233,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';
|
||||
|
||||
@@ -395,49 +249,6 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get settings_privacyModeDisabled => 'Ochranný režim je vypnutý';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Nastavenia súkromia';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle => 'Ovládni, aké informácie sa zdieľajú.';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Vyberte, ktoré informácie váš zariadenie zdieľa s ostatnými.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Zamietnuť všetko';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Povoliť podľa kontaktových vlajok';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Povoliť všetko';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Základný režim telemetrie';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode => 'Režim umiestnenia telemetrie';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode => 'Režim prostredia telemetrie';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Umiestnenie inzerátu';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => 'Zahrnúť polohu do inzerátu';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Viaceré ACK: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated =>
|
||||
'Režim telemetrie bol aktualizovaný';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Možné akcie';
|
||||
|
||||
@@ -937,9 +748,6 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'Skupina musí mať názov.';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => 'Tento názov skupiny je rezervovaný';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'Skupina \"$name\" už existuje';
|
||||
@@ -981,41 +789,6 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
return 'Posledné zobrazenie $days dní dozadu';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Kontaktné informácie';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Nastavenia kontaktov';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Telemetria';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Naposledy videný';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Vymazať chat';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Báza telemetrie';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Povoliť zdieľanie úrovne batérie a základnej telemetrie';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Lokácia telemetrie';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle => 'Povoliť zdieľanie údajov o lokalite';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Prostredie telemetrie';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle =>
|
||||
'Povoliť zdieľanie údajov senzorov prostredia';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Kanály';
|
||||
|
||||
@@ -1585,9 +1358,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';
|
||||
|
||||
@@ -1648,16 +1418,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';
|
||||
|
||||
@@ -3338,82 +3098,4 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Naposledy videný';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Nastavenia kontaktov';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Automatické zisťovanie';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Ďalšie nastavenia súvisiace s kontaktami';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Automaticky pridávať užívateľov';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Povoliť spoločníkovi automaticky pridávať objavených užívateľov.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Automaticky pridávať opakovače';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Povoliť spoločníkovi automaticky pridávať objavené repeater.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Automaticky pridávať server miestnosti';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Povoliť spoločníkovi automaticky pridať objavené serverové miestnosti.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Automaticky pridávať senzory';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Povoliť spoločníkovi automaticky pridávať objavené senzory.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle => 'Prepísať najstaršie';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'Keď je zoznam kontaktov plný, bude nahradený najstarší neoznačený kontakt.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Objavené kontakty';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => 'Žiadne zhodné kontakty';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Vyhľadať objavené kontakty';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Kontakt bol pridaný';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Pridať kontakt';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact => 'Kopírovať kontakt do schránky';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Zmazať kontakt';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Zmazať všetky objavené kontakty';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Ste si istí, že chcete zmazať všetky objavené kontakty?';
|
||||
}
|
||||
|
||||
@@ -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...';
|
||||
|
||||
@@ -280,13 +150,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Prosimo, vklopite Bluetooth, da lahko poiščete naprave.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Zahtevan brskalnik Chrome';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Ta spletna aplikacija za podporo Bluetooth zahteva Google Chrome ali brskalnik na osnovi Chromiuma.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Omogočite Bluetooth';
|
||||
|
||||
@@ -370,13 +233,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';
|
||||
|
||||
@@ -393,50 +249,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_privacyModeDisabled => 'Privatni način je onemogočen.';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Nastavitve zasebnosti';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle =>
|
||||
'Kontrolirajte, katere informacije so deljene.';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Izberite, katere informacije vaš naprava deli z drugimi.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Zavrniti vse';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Dovoli po kontaktnih zastavah';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Dovoli vse';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Osnovni način telemetrije';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode => 'Način delovanja telemetrije';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode =>
|
||||
'Način delovanja okolja telemetrije';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Lokacija oglasa';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => 'Vključi lokacijo v oglas.';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Večkratni potrditvi: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Način telemetrije posodobljen';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Akcije';
|
||||
|
||||
@@ -936,9 +748,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'Ime skupine je obvezno.';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => 'To ime skupine je rezervirano';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'Skupina \"$name\" že obstaja';
|
||||
@@ -978,41 +787,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
return 'Zadnjič viden pred $days dnem';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Kontaktni podatki';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Nastavitve stika';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Telemetrija';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Zadnjič videno';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Počisti klepet';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Baza telemetrije';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Dovoli deljenje stanja baterije in osnovne telemetrije';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Lokacija telemetrije';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle => 'Dovoli deljenje podatkov o lokaciji';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Okolje telemetrije';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle =>
|
||||
'Dovoli deljenje podatkov okoljskih senzorjev';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Kanali';
|
||||
|
||||
@@ -1580,9 +1354,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';
|
||||
|
||||
@@ -1643,15 +1414,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';
|
||||
|
||||
@@ -3341,81 +3103,4 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Zadnjič videno';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Nastavitve stikov';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Avtomatsko odkrivanje';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle => 'Druge nastavitve v zvezi s stiki';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Avtomatsko dodaj uporabnike';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Dovoli spremljevalcu, da samodejno doda odkrite uporabnike.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Avtomatsko dodaj ponovitelje';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Dovoli spremljevalcu, da samodejno doda odkrite ponovitelje.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Avtomatsko dodaj strežnike sob';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Dovoli spremljevalcu, da samodejno doda odkrite strežnike sob.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Avtomatsko dodaj senzorje';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Dovoli spremljevalcu, da samodejno doda odkrite senzorje.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle => 'Prepiši najstarejše';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'Ko je seznam stikov poln, bo najstarejši nestarševski stik zamenjan.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Odkriti stiki';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => 'Ni ujemajočih stikov';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Najdeni stiki po iskanju';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Kontakt dodan';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Dodaj stik';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact => 'Kopiraj stik v odložišče';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Izbriši stik';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Izbriši vse odkrite kontakte';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Ste prepričani, da želite izbrisati vse odkrite kontakte?';
|
||||
}
|
||||
|
||||
@@ -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...';
|
||||
|
||||
@@ -279,13 +149,6 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Vänligen aktivera Bluetooth för att söka efter enheter.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Chrome-webbläsare krävs';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Denna webbapplikation kräver Google Chrome oder en Chromium-baserader webbläsare för Bluetooth-stöd.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Aktivera Bluetooth';
|
||||
|
||||
@@ -369,13 +232,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';
|
||||
|
||||
@@ -392,49 +248,6 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get settings_privacyModeDisabled => 'Privatläge är avstängt';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Inställningar för sekretess';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle =>
|
||||
'Kontrollera vilken information som delas.';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Välj vilken information din enhet delar med andra.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Neka alla';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Tillåt via kontaktflaggor';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Tillåt alla';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Telemetribasläge';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode => 'Telemetritillstånd för plats';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode => 'Telemetri miljöläge';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Annonsplacering';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => 'Inkludera plats i annonsen';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Multi-ACKs: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Telemetri-läge uppdaterat';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Åtgärder';
|
||||
|
||||
@@ -931,9 +744,6 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'Gruppnamnet är obligatoriskt';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => 'Detta gruppnamn är reserverat';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'Gruppen \"$name\" finns redan.';
|
||||
@@ -973,40 +783,6 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
return 'Senast synlig $days dagar sedan';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Kontaktinformation';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Kontaktinställningar';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Telemetri';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Senast sedd';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Rensa Chatt';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Telemetribas';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Tillåt delning av batterinivå och grundläggande telemetri';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Telemetridata plats';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle => 'Tillåt delning av platsdata';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Telemetri Miljö';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle => 'Tillåt delning av miljösensordata';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Kanaler';
|
||||
|
||||
@@ -1574,9 +1350,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';
|
||||
|
||||
@@ -1637,16 +1410,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';
|
||||
|
||||
@@ -3318,82 +3081,4 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Senast sedd';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Kontaktinställningar';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Automatisk upptäckt';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Andra inställningar relaterade till kontakt';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Lägg till användare automatiskt';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Tillåt kompanjonen att automatiskt lägga till upptäckta användare';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Lägg till upprepande enheter automatiskt';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Tillåt kompanjonen att automatiskt lägga till upptäckta repeater.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Lägg automatiskt till rumsservrar';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Tillåt kompanjonen att automatiskt lägga till upptäckta rumsservrar.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Lägg till sensorer automatiskt';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Tillåt kompanjonen att automatiskt lägga till upptäckta sensorer.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle => 'Skriv över äldst';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'När kontaktlistan är full ersätts den äldsta icke-favoriterade kontakten.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Upptäckta kontakter';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => 'Inga matchande kontakter';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Sök uppfunna kontakter';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Kontakt tillagd';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Lägg till kontakt';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact => 'Kopiera kontakt till urklipp';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Ta bort kontakt';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Ta bort alla upptäckta kontakter';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Är du säker på att du vill ta bort alla upptäckta kontakter?';
|
||||
}
|
||||
|
||||
@@ -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 => 'Пошук пристроїв...';
|
||||
|
||||
@@ -282,13 +150,6 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Потрібен браузер Chrome';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Увімкніть Bluetooth';
|
||||
|
||||
@@ -371,13 +232,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 => 'Режим приватності';
|
||||
|
||||
@@ -395,50 +249,6 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get settings_privacyModeDisabled => 'Режим приватності вимкнено';
|
||||
|
||||
@override
|
||||
String get settings_privacy => 'Налаштування приватності';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle =>
|
||||
'Керуйте інформацією, яку буде спільно використовуватися';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription =>
|
||||
'Виберіть, яку інформацію ваш пристрій буде передавати іншим.';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => 'Відхилити все';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => 'Дозволити за контактними прапорцями';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => 'Дозволити все';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => 'Режим базової телеметрії';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode => 'Режим місця телеметрії';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode => 'Режим середовища телеметрії';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => 'Розміщення реклами';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Включити місце розташування в оголошення';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return 'Багатократне підтвердження: $value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => 'Режим телеметрії оновлено';
|
||||
|
||||
@override
|
||||
String get settings_actions => 'Дії';
|
||||
|
||||
@@ -942,9 +752,6 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => 'Назва групи обов\'язкова.';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => 'Ця назва групи зарезервована';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return 'Група «$name» вже існує.';
|
||||
@@ -984,42 +791,6 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
return 'В мережі $days дн. тому';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => 'Контактна інформація';
|
||||
|
||||
@override
|
||||
String get contact_settings => 'Налаштування контактів';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => 'Телеметрія';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => 'Останній раз бачили';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => 'Очистити чат';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => 'Базовий телебачення';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle =>
|
||||
'Дозволити спільний доступ до рівня заряду батареї та базової телеметрії';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => 'Розташування телеметрії';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle =>
|
||||
'Дозволити спільне використання даних про місцеположення';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => 'Середовище телеметрії';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle =>
|
||||
'Дозволити спільний доступ до даних датчиків середовища';
|
||||
|
||||
@override
|
||||
String get channels_title => 'Канали';
|
||||
|
||||
@@ -1593,9 +1364,6 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get map_shareMarkerHere => 'Поділитися маркером тут';
|
||||
|
||||
@override
|
||||
String get map_setAsMyLocation => 'Встановити моє місцезнаходження';
|
||||
|
||||
@override
|
||||
String get map_pinLabel => 'Мітка піна';
|
||||
|
||||
@@ -1656,16 +1424,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 => 'Час останньої активності';
|
||||
|
||||
@@ -3372,84 +3130,4 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Останній раз бачили';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Налаштування контактів';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Автоматичне виявлення';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Інші налаштування, пов\'язані з контактами';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Автоматично додавати користувачів';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Дозволити супутникові автоматично додавати виявлених користувачів';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Автоматично додавати повторювачі';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Дозволити супутнику автоматично додавати виявлені ретранслятори';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Автоматично додавати сервери кімнат';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Дозволити супровіднику автоматично додавати виявлені сервери кімнат.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Автоматично додавати датчики';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Дозволити супровіднику автоматично додавати виявлені сенсори';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle => 'Перезаписати найстаріше';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'Коли список контактів заповнений, найстарший контакт без позначки улюбленого буде замінений.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Виявлені контакти';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching =>
|
||||
'Відповідних контактів не знайдено';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Знайти виявлені контакти';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Контакт додано';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Додати контакт';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact =>
|
||||
'Копіювати контакт у буфер обміну';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Видалити контакт';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Видалити всі виявлені контакти';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Ви впевнені, що хочете видалити всі виявлені контакти?';
|
||||
}
|
||||
|
||||
@@ -38,9 +38,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get common_delete => '删除';
|
||||
|
||||
@override
|
||||
String get common_deleteAll => '删除全部';
|
||||
|
||||
@override
|
||||
String get common_close => '关闭';
|
||||
|
||||
@@ -111,123 +108,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => '连接设备';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => '蓝牙';
|
||||
|
||||
@override
|
||||
String get connectionChoiceTcpLabel => 'TCP';
|
||||
|
||||
@override
|
||||
String get tcpScreenTitle => '通过 TCP 连接';
|
||||
|
||||
@override
|
||||
String get tcpHostLabel => 'IP地址';
|
||||
|
||||
@override
|
||||
String get tcpHostHint => '192.168.40.10';
|
||||
|
||||
@override
|
||||
String get tcpPortLabel => '端口';
|
||||
|
||||
@override
|
||||
String get tcpPortHint => '5000';
|
||||
|
||||
@override
|
||||
String get tcpStatus_notConnected => '输入目标地址,然后连接';
|
||||
|
||||
@override
|
||||
String tcpStatus_connectingTo(String endpoint) {
|
||||
return '连接到 $endpoint...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tcpErrorHostRequired => '需要提供IP地址。';
|
||||
|
||||
@override
|
||||
String get tcpErrorPortInvalid => '端口号必须在 1 到 65535 之间。';
|
||||
|
||||
@override
|
||||
String get tcpErrorUnsupported => '此平台不支持 TCP 传输。';
|
||||
|
||||
@override
|
||||
String get tcpErrorTimedOut => 'TCP 连接超时。';
|
||||
|
||||
@override
|
||||
String tcpConnectionFailed(String error) {
|
||||
return 'TCP 连接失败:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => '通过USB连接';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle => '选择已检测到的串行设备,并直接连接到您的 MeshCore 节点。';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => '选择一个 USB 设备';
|
||||
|
||||
@override
|
||||
String get usbScreenNote => 'USB 串行接口在支持的 Android 设备和桌面平台上处于活动状态。';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState => '未找到任何 USB 设备。请插入一个,然后刷新。';
|
||||
|
||||
@override
|
||||
String get usbErrorPermissionDenied => '拒绝了USB权限。';
|
||||
|
||||
@override
|
||||
String get usbErrorDeviceMissing => '所选的USB设备已不再可用。';
|
||||
|
||||
@override
|
||||
String get usbErrorInvalidPort => '选择一个有效的USB设备。';
|
||||
|
||||
@override
|
||||
String get usbErrorBusy => '还有一个 USB 连接请求正在进行中。';
|
||||
|
||||
@override
|
||||
String get usbErrorNotConnected => '没有连接任何USB设备。';
|
||||
|
||||
@override
|
||||
String get usbErrorOpenFailed => '未能打开所选的USB设备。';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectFailed => '未能连接到所选的USB设备。';
|
||||
|
||||
@override
|
||||
String get usbErrorUnsupported => '此平台不支持USB串行通信。';
|
||||
|
||||
@override
|
||||
String get usbErrorAlreadyActive => 'USB 连接已建立。';
|
||||
|
||||
@override
|
||||
String get usbErrorNoDeviceSelected => '未选择任何 USB 设备。';
|
||||
|
||||
@override
|
||||
String get usbErrorPortClosed => 'USB 连接未建立。';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectTimedOut => '连接超时。请确保设备已安装 USB 伴侣固件。';
|
||||
|
||||
@override
|
||||
String get usbFallbackDeviceName => 'Web 串流设备';
|
||||
|
||||
@override
|
||||
String get usbStatus_notConnected => '选择一个 USB 设备';
|
||||
|
||||
@override
|
||||
String get usbStatus_connecting => '连接USB设备...';
|
||||
|
||||
@override
|
||||
String get usbStatus_searching => '正在搜索 USB 设备...';
|
||||
|
||||
@override
|
||||
String usbConnectionFailed(String error) {
|
||||
return 'USB 连接失败:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanner_scanning => '正在搜索设备...';
|
||||
|
||||
@@ -268,13 +148,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage => '请开启蓝牙以搜索设备';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => '需要 Chrome 浏览器';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'此 Web 应用程序需要 Google Chrome 或基于 Chromium 的浏览器以支持蓝牙。';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => '启用蓝牙';
|
||||
|
||||
@@ -353,12 +226,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get settings_longitude => '经度';
|
||||
|
||||
@override
|
||||
String get settings_contactSettings => '联系人设置';
|
||||
|
||||
@override
|
||||
String get settings_contactSettingsSubtitle => '添加联系人的设置';
|
||||
|
||||
@override
|
||||
String get settings_privacyMode => '隐私模式';
|
||||
|
||||
@@ -374,47 +241,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get settings_privacyModeDisabled => '隐私模式已关闭';
|
||||
|
||||
@override
|
||||
String get settings_privacy => '隐私设置';
|
||||
|
||||
@override
|
||||
String get settings_privacySubtitle => '控制要共享的信息。';
|
||||
|
||||
@override
|
||||
String get settings_privacySettingsDescription => '选择您的设备与他人共享的信息。';
|
||||
|
||||
@override
|
||||
String get settings_denyAll => '拒绝所有';
|
||||
|
||||
@override
|
||||
String get settings_allowByContact => '按联系人标志允许';
|
||||
|
||||
@override
|
||||
String get settings_allowAll => '允许全部';
|
||||
|
||||
@override
|
||||
String get settings_telemetryBaseMode => '遥测基础模式';
|
||||
|
||||
@override
|
||||
String get settings_telemetryLocationMode => '遥测位置模式';
|
||||
|
||||
@override
|
||||
String get settings_telemetryEnvironmentMode => '遥测环境模式';
|
||||
|
||||
@override
|
||||
String get settings_advertLocation => '广告位置';
|
||||
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => '在广告中包含位置';
|
||||
|
||||
@override
|
||||
String settings_multiAck(String value) {
|
||||
return '多重ACK:$value';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_telemetryModeUpdated => '遥测模式已更新';
|
||||
|
||||
@override
|
||||
String get settings_actions => '操作';
|
||||
|
||||
@@ -886,9 +712,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get contacts_groupNameRequired => '请输入群聊名称';
|
||||
|
||||
@override
|
||||
String get contacts_groupNameReserved => '该群组名称已被保留';
|
||||
|
||||
@override
|
||||
String contacts_groupAlreadyExists(String name) {
|
||||
return '名为 \"$name\" 的群聊已存在';
|
||||
@@ -927,39 +750,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
return '最后在线 $days 天前';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contact_info => '联系信息';
|
||||
|
||||
@override
|
||||
String get contact_settings => '联系人设置';
|
||||
|
||||
@override
|
||||
String get contact_telemetry => '遥测数据';
|
||||
|
||||
@override
|
||||
String get contact_lastSeen => '最近出现';
|
||||
|
||||
@override
|
||||
String get contact_clearChat => '清除聊天记录';
|
||||
|
||||
@override
|
||||
String get contact_teleBase => '遥测基站';
|
||||
|
||||
@override
|
||||
String get contact_teleBaseSubtitle => '允许共享电池电量和基本遥测数据';
|
||||
|
||||
@override
|
||||
String get contact_teleLoc => '遥测位置';
|
||||
|
||||
@override
|
||||
String get contact_teleLocSubtitle => '允许共享位置数据';
|
||||
|
||||
@override
|
||||
String get contact_teleEnv => '遥测环境';
|
||||
|
||||
@override
|
||||
String get contact_teleEnvSubtitle => '允许共享环境传感器数据';
|
||||
|
||||
@override
|
||||
String get channels_title => '频道';
|
||||
|
||||
@@ -1498,9 +1288,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get map_shareMarkerHere => '在此分享标记';
|
||||
|
||||
@override
|
||||
String get map_setAsMyLocation => '设置为我的位置';
|
||||
|
||||
@override
|
||||
String get map_pinLabel => '标签';
|
||||
|
||||
@@ -1560,15 +1347,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get map_showSharedMarkers => '显示共享标记';
|
||||
|
||||
@override
|
||||
String get map_showGuessedLocations => '显示猜测的节点位置';
|
||||
|
||||
@override
|
||||
String get map_showDiscoveryContacts => '显示发现联系人';
|
||||
|
||||
@override
|
||||
String get map_guessedLocation => '猜测的位置';
|
||||
|
||||
@override
|
||||
String get map_lastSeenTime => '最后在线时间';
|
||||
|
||||
@@ -3112,71 +2890,4 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => '最近访问';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => '联系人设置';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => '自动发现';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle => '其他联系人相关设置';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle => '自动添加用户';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle => '允许伴侣自动添加发现的用户';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle => '自动添加重复器';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle => '允许伴侣自动添加发现的重复器';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle => '自动添加房间服务器';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle => '允许伴侣自动添加发现的房间服务器';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle => '自动添加传感器';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle => '允许伴侣自动添加发现的传感器';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle => '覆盖最旧的';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'当联系人列表已满时,将替换最老的非收藏联系人。';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => '已发现的联系人';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => '没有匹配的联系人';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => '搜索已发现的联系人';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => '联系人已添加';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => '添加联系人';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact => '复制联系人到剪贴板';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => '删除联系人';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll => '删除所有发现的联系人';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAllContent => '您确定要删除所有发现的联系人吗?';
|
||||
}
|
||||
|
||||
+1
-122
@@ -285,7 +285,6 @@
|
||||
"contacts_newGroup": "Nieuwe Groep",
|
||||
"contacts_groupName": "Groepnaam",
|
||||
"contacts_groupNameRequired": "De groepnaam is verplicht.",
|
||||
"contacts_groupNameReserved": "Deze groepsnaam is gereserveerd",
|
||||
"contacts_groupAlreadyExists": "De groep \"{name}\" bestaat al.",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -1606,8 +1605,6 @@
|
||||
"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",
|
||||
@@ -1802,123 +1799,5 @@
|
||||
"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",
|
||||
"settings_privacy": "Privacyinstellingen",
|
||||
"settings_privacySubtitle": "Beheer welke informatie wordt gedeeld",
|
||||
"settings_telemetryLocationMode": "Telemetrie-locatiemodus",
|
||||
"settings_telemetryEnvironmentMode": "Telemetrie-omgevingsmodus",
|
||||
"settings_advertLocation": "Advertentielocatie",
|
||||
"settings_advertLocationSubtitle": "Locatie opnemen in advertentie",
|
||||
"settings_privacySettingsDescription": "Kies welke informatie uw apparaat deelt met anderen",
|
||||
"settings_allowByContact": "Toestaan op basis van contactvlaggen",
|
||||
"settings_allowAll": "Alles toestaan",
|
||||
"settings_denyAll": "Weiger alles",
|
||||
"contact_info": "Contactinformatie",
|
||||
"settings_telemetryBaseMode": "Telemetrie-basismodus",
|
||||
"contact_teleBase": "Telemetrie_basis",
|
||||
"contact_teleLoc": "Telemetrielocatie",
|
||||
"contact_teleLocSubtitle": "Locatiegegevens delen toestaan",
|
||||
"contact_teleEnv": "Telemetrieomgeving",
|
||||
"contact_teleEnvSubtitle": "Delen van omgevingsensordata toestaan",
|
||||
"contact_settings": "Contactinstellingen",
|
||||
"contact_telemetry": "Telemetrie",
|
||||
"contact_lastSeen": "Laatst gezien",
|
||||
"contact_clearChat": "Chat leegmaken",
|
||||
"contact_teleBaseSubtitle": "Sta delen van batterij niveau en basis telemetrie toe",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_telemetryModeUpdated": "Telemetrie-modus bijgewerkt",
|
||||
"settings_multiAck": "Multi-ACKs: {value}"
|
||||
"contacts_searchRoomServers": "Zoek {number}{str} Room servers..."
|
||||
}
|
||||
|
||||
+1
-122
@@ -285,7 +285,6 @@
|
||||
"contacts_newGroup": "Nowa Grupa",
|
||||
"contacts_groupName": "Nazwa grupy",
|
||||
"contacts_groupNameRequired": "Nazwa grupy jest wymagana",
|
||||
"contacts_groupNameReserved": "Ta nazwa grupy jest zastrzeżona",
|
||||
"contacts_groupAlreadyExists": "Grupa \"{name}\" już istnieje",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -1605,8 +1604,6 @@
|
||||
"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",
|
||||
@@ -1802,123 +1799,5 @@
|
||||
"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ę",
|
||||
"settings_allowByContact": "Zezwalaj według flag kontaktowych",
|
||||
"settings_allowAll": "Zezwalaj na wszystko",
|
||||
"settings_telemetryLocationMode": "Tryb położenia telemetrycznego",
|
||||
"settings_telemetryEnvironmentMode": "Tryb środowiska telemetrycznego",
|
||||
"settings_advertLocation": "Lokalizacja reklamowa",
|
||||
"settings_advertLocationSubtitle": "Uwzględnij lokalizację w ogłoszeniu",
|
||||
"settings_denyAll": "Odmów wszystkim",
|
||||
"settings_privacySubtitle": "Kontroluj jakie informacje są udostępniane.",
|
||||
"settings_privacy": "Ustawienia prywatności",
|
||||
"settings_privacySettingsDescription": "Wybierz jakie informacje urządzenie udostępni innym.",
|
||||
"contact_info": "Informacje kontaktowe",
|
||||
"settings_telemetryBaseMode": "Tryb podstawowy telemetrii",
|
||||
"contact_teleBase": "Baza telemetryczna",
|
||||
"contact_teleLoc": "Lokalizacja telemetryczna",
|
||||
"contact_teleLocSubtitle": "Zezwalaj na udostępnianie danych lokalizacji",
|
||||
"contact_teleEnv": "Środowisko telemetryczne",
|
||||
"contact_teleEnvSubtitle": "Zezwalaj na udostępnianie danych czujników środowiskowych",
|
||||
"contact_telemetry": "Telemetryka",
|
||||
"contact_clearChat": "Wyczyść czat",
|
||||
"contact_settings": "Ustawienia kontaktowe",
|
||||
"contact_lastSeen": "Ostatnio widziany",
|
||||
"contact_teleBaseSubtitle": "Pozwól na udostępnianie poziomu naładowania baterii i podstawowych danych telemetrycznych",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_telemetryModeUpdated": "Tryb telemetryczny zaktualizowany",
|
||||
"settings_multiAck": "Wiele potwierdzeń: {value}"
|
||||
"contacts_searchRepeaters": "Wyszukaj {number}{str} powtórników..."
|
||||
}
|
||||
|
||||
+1
-122
@@ -285,7 +285,6 @@
|
||||
"contacts_newGroup": "Novo Grupo",
|
||||
"contacts_groupName": "Nome do grupo",
|
||||
"contacts_groupNameRequired": "O nome do grupo é obrigatório.",
|
||||
"contacts_groupNameReserved": "Este nome de grupo está reservado",
|
||||
"contacts_groupAlreadyExists": "O grupo \"{name}\" já existe",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -1607,8 +1606,6 @@
|
||||
"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",
|
||||
@@ -1802,123 +1799,5 @@
|
||||
"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",
|
||||
"settings_privacySettingsDescription": "Escolha quais informações o seu dispositivo compartilha com os outros.",
|
||||
"settings_allowByContact": "Permitir por bandeiras de contato",
|
||||
"settings_telemetryLocationMode": "Modo de Localização de Telemetria",
|
||||
"settings_telemetryEnvironmentMode": "Modo de Ambiente de Telemetria",
|
||||
"settings_advertLocation": "Localização do Anúncio",
|
||||
"settings_advertLocationSubtitle": "Incluir localização no anúncio",
|
||||
"settings_privacySubtitle": "Controle o que é compartilhado.",
|
||||
"settings_denyAll": "Negar todos",
|
||||
"settings_allowAll": "Permitir todos",
|
||||
"settings_privacy": "Configurações de Privacidade",
|
||||
"contact_info": "Informações de Contato",
|
||||
"settings_telemetryBaseMode": "Modo Base de Telemetria",
|
||||
"contact_teleBase": "Base de Telemetria",
|
||||
"contact_teleLoc": "Localização de Telemetria",
|
||||
"contact_teleLocSubtitle": "Permitir compartilhamento de dados de localização",
|
||||
"contact_teleEnv": "Ambiente de Telemetria",
|
||||
"contact_teleEnvSubtitle": "Permitir compartilhamento de dados do sensor de ambiente",
|
||||
"contact_lastSeen": "Visto pela última vez",
|
||||
"contact_clearChat": "Limpar Chat",
|
||||
"contact_telemetry": "Telemetria",
|
||||
"contact_settings": "Configurações de Contato",
|
||||
"contact_teleBaseSubtitle": "Permitir compartilhamento do nível da bateria e telemetria básica",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_telemetryModeUpdated": "Modo de telemetria atualizado",
|
||||
"settings_multiAck": "Multi-ACKs: {value}"
|
||||
"contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala..."
|
||||
}
|
||||
|
||||
+1
-122
@@ -212,7 +212,6 @@
|
||||
"contacts_newGroup": "Новая группа",
|
||||
"contacts_groupName": "Имя группы",
|
||||
"contacts_groupNameRequired": "Имя группы обязательно",
|
||||
"contacts_groupNameReserved": "Это имя группы зарезервировано",
|
||||
"contacts_groupAlreadyExists": "Группа \"{name}\" уже существует",
|
||||
"contacts_filterContacts": "Фильтр контактов...",
|
||||
"contacts_noContactsMatchFilter": "Нет контактов, соответствующих фильтру",
|
||||
@@ -847,8 +846,6 @@
|
||||
"scanner_enableBluetooth": "Включите Bluetooth",
|
||||
"scanner_bluetoothOff": "Bluetooth выключен",
|
||||
"scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства.",
|
||||
"scanner_chromeRequired": "Требуется браузер Chrome",
|
||||
"scanner_chromeRequiredMessage": "Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.",
|
||||
"snrIndicator_nearByRepeaters": "Ближайшие ретрансляторы",
|
||||
"snrIndicator_lastSeen": "Последний раз видели",
|
||||
"chat_ShowAllPaths": "Показать все пути",
|
||||
@@ -1042,123 +1039,5 @@
|
||||
"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": "Установить мое местоположение",
|
||||
"settings_privacy": "Настройки конфиденциальности",
|
||||
"settings_privacySubtitle": "Контролируйте, какую информацию делиться.",
|
||||
"settings_telemetryLocationMode": "Режим местоположения телеметрии",
|
||||
"settings_telemetryEnvironmentMode": "Режим среды телеметрии",
|
||||
"settings_advertLocation": "Местоположение рекламы",
|
||||
"settings_advertLocationSubtitle": "Включить местоположение в объявление",
|
||||
"settings_allowAll": "Разрешить все",
|
||||
"settings_privacySettingsDescription": "Выберите, какую информацию ваше устройство будет делиться с другими.",
|
||||
"settings_denyAll": "Отклонить все",
|
||||
"settings_allowByContact": "Разрешить по флагам контактов",
|
||||
"contact_info": "Контактная информация",
|
||||
"settings_telemetryBaseMode": "Базовый режим телеметрии",
|
||||
"contact_teleBase": "База телеметрии",
|
||||
"contact_teleLoc": "Местоположение телеметрии",
|
||||
"contact_teleLocSubtitle": "Разрешить обмен данными о местоположении",
|
||||
"contact_teleEnv": "Среда телеметрии",
|
||||
"contact_teleEnvSubtitle": "Разрешить обмен данными датчиков окружающей среды",
|
||||
"contact_settings": "Настройки контактов",
|
||||
"contact_telemetry": "Телеметрия",
|
||||
"contact_clearChat": "Очистить чат",
|
||||
"contact_lastSeen": "Последний раз видели",
|
||||
"contact_teleBaseSubtitle": "Разрешить обмен уровнем заряда батареи и базовой телеметрией",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_telemetryModeUpdated": "Режим телеметрии обновлен",
|
||||
"settings_multiAck": "Мульти-ACK: {value}"
|
||||
"contacts_searchUsers": "Поиск {number}{str} пользователей..."
|
||||
}
|
||||
|
||||
+1
-122
@@ -285,7 +285,6 @@
|
||||
"contacts_newGroup": "Nová skupina",
|
||||
"contacts_groupName": "Názov skupiny",
|
||||
"contacts_groupNameRequired": "Skupina musí mať názov.",
|
||||
"contacts_groupNameReserved": "Tento názov skupiny je rezervovaný",
|
||||
"contacts_groupAlreadyExists": "Skupina \"{name}\" už existuje",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -1605,8 +1604,6 @@
|
||||
"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ý",
|
||||
@@ -1802,123 +1799,5 @@
|
||||
"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",
|
||||
"settings_privacy": "Nastavenia súkromia",
|
||||
"settings_privacySubtitle": "Ovládni, aké informácie sa zdieľajú.",
|
||||
"settings_telemetryLocationMode": "Režim umiestnenia telemetrie",
|
||||
"settings_telemetryBaseMode": "Základný režim telemetrie",
|
||||
"settings_advertLocation": "Umiestnenie inzerátu",
|
||||
"settings_telemetryEnvironmentMode": "Režim prostredia telemetrie",
|
||||
"settings_advertLocationSubtitle": "Zahrnúť polohu do inzerátu",
|
||||
"settings_allowAll": "Povoliť všetko",
|
||||
"settings_privacySettingsDescription": "Vyberte, ktoré informácie váš zariadenie zdieľa s ostatnými.",
|
||||
"settings_denyAll": "Zamietnuť všetko",
|
||||
"settings_allowByContact": "Povoliť podľa kontaktových vlajok",
|
||||
"contact_info": "Kontaktné informácie",
|
||||
"contact_settings": "Nastavenia kontaktov",
|
||||
"contact_teleBaseSubtitle": "Povoliť zdieľanie úrovne batérie a základnej telemetrie",
|
||||
"contact_teleLoc": "Lokácia telemetrie",
|
||||
"contact_teleLocSubtitle": "Povoliť zdieľanie údajov o lokalite",
|
||||
"contact_teleEnv": "Prostredie telemetrie",
|
||||
"contact_telemetry": "Telemetria",
|
||||
"contact_clearChat": "Vymazať chat",
|
||||
"contact_lastSeen": "Naposledy videný",
|
||||
"contact_teleBase": "Báza telemetrie",
|
||||
"contact_teleEnvSubtitle": "Povoliť zdieľanie údajov senzorov prostredia",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_telemetryModeUpdated": "Režim telemetrie bol aktualizovaný",
|
||||
"settings_multiAck": "Viaceré ACK: {value}"
|
||||
"contacts_unread": "Neprečítané"
|
||||
}
|
||||
|
||||
+1
-122
@@ -285,7 +285,6 @@
|
||||
"contacts_newGroup": "Nova skupina",
|
||||
"contacts_groupName": "Ime skupine",
|
||||
"contacts_groupNameRequired": "Ime skupine je obvezno.",
|
||||
"contacts_groupNameReserved": "To ime skupine je rezervirano",
|
||||
"contacts_groupAlreadyExists": "Skupina \"{name}\" že obstaja",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -1606,8 +1605,6 @@
|
||||
"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",
|
||||
@@ -1802,123 +1799,5 @@
|
||||
"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",
|
||||
"settings_privacy": "Nastavitve zasebnosti",
|
||||
"settings_privacySettingsDescription": "Izberite, katere informacije vaš naprava deli z drugimi.",
|
||||
"settings_telemetryBaseMode": "Osnovni način telemetrije",
|
||||
"settings_telemetryLocationMode": "Način delovanja telemetrije",
|
||||
"settings_telemetryEnvironmentMode": "Način delovanja okolja telemetrije",
|
||||
"settings_advertLocation": "Lokacija oglasa",
|
||||
"settings_allowByContact": "Dovoli po kontaktnih zastavah",
|
||||
"settings_denyAll": "Zavrniti vse",
|
||||
"settings_allowAll": "Dovoli vse",
|
||||
"settings_privacySubtitle": "Kontrolirajte, katere informacije so deljene.",
|
||||
"contact_info": "Kontaktni podatki",
|
||||
"contact_teleBase": "Baza telemetrije",
|
||||
"contact_teleBaseSubtitle": "Dovoli deljenje stanja baterije in osnovne telemetrije",
|
||||
"contact_teleLoc": "Lokacija telemetrije",
|
||||
"contact_lastSeen": "Zadnjič videno",
|
||||
"contact_settings": "Nastavitve stika",
|
||||
"settings_advertLocationSubtitle": "Vključi lokacijo v oglas.",
|
||||
"contact_telemetry": "Telemetrija",
|
||||
"contact_clearChat": "Počisti klepet",
|
||||
"contact_teleEnv": "Okolje telemetrije",
|
||||
"contact_teleEnvSubtitle": "Dovoli deljenje podatkov okoljskih senzorjev",
|
||||
"contact_teleLocSubtitle": "Dovoli deljenje podatkov o lokaciji",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_multiAck": "Večkratni potrditvi: {value}",
|
||||
"settings_telemetryModeUpdated": "Način telemetrije posodobljen"
|
||||
"contacts_searchUsers": "Išči {number}{str} uporabnikov..."
|
||||
}
|
||||
|
||||
+1
-122
@@ -285,7 +285,6 @@
|
||||
"contacts_newGroup": "Ny grupp",
|
||||
"contacts_groupName": "Gruppnamn",
|
||||
"contacts_groupNameRequired": "Gruppnamnet är obligatoriskt",
|
||||
"contacts_groupNameReserved": "Detta gruppnamn är reserverat",
|
||||
"contacts_groupAlreadyExists": "Gruppen \"{name}\" finns redan.",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -1606,8 +1605,6 @@
|
||||
"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",
|
||||
@@ -1802,123 +1799,5 @@
|
||||
"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",
|
||||
"settings_privacy": "Inställningar för sekretess",
|
||||
"settings_allowAll": "Tillåt alla",
|
||||
"settings_privacySubtitle": "Kontrollera vilken information som delas.",
|
||||
"settings_telemetryEnvironmentMode": "Telemetri miljöläge",
|
||||
"settings_telemetryBaseMode": "Telemetribasläge",
|
||||
"settings_telemetryLocationMode": "Telemetritillstånd för plats",
|
||||
"settings_advertLocation": "Annonsplacering",
|
||||
"contact_info": "Kontaktinformation",
|
||||
"contact_settings": "Kontaktinställningar",
|
||||
"contact_telemetry": "Telemetri",
|
||||
"settings_denyAll": "Neka alla",
|
||||
"settings_allowByContact": "Tillåt via kontaktflaggor",
|
||||
"settings_privacySettingsDescription": "Välj vilken information din enhet delar med andra.",
|
||||
"contact_lastSeen": "Senast sedd",
|
||||
"contact_clearChat": "Rensa Chatt",
|
||||
"contact_teleEnv": "Telemetri Miljö",
|
||||
"settings_advertLocationSubtitle": "Inkludera plats i annonsen",
|
||||
"contact_teleEnvSubtitle": "Tillåt delning av miljösensordata",
|
||||
"contact_teleBase": "Telemetribas",
|
||||
"contact_teleBaseSubtitle": "Tillåt delning av batterinivå och grundläggande telemetri",
|
||||
"contact_teleLoc": "Telemetridata plats",
|
||||
"contact_teleLocSubtitle": "Tillåt delning av platsdata",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_telemetryModeUpdated": "Telemetri-läge uppdaterat",
|
||||
"settings_multiAck": "Multi-ACKs: {value}"
|
||||
"contacts_searchRoomServers": "Sök {number}{str} Room-servrar..."
|
||||
}
|
||||
|
||||
+1
-122
@@ -286,7 +286,6 @@
|
||||
"contacts_newGroup": "Нова група",
|
||||
"contacts_groupName": "Назва групи",
|
||||
"contacts_groupNameRequired": "Назва групи обов'язкова.",
|
||||
"contacts_groupNameReserved": "Ця назва групи зарезервована",
|
||||
"contacts_groupAlreadyExists": "Група «{name}» вже існує.",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -1606,8 +1605,6 @@
|
||||
"map_pathTraceCancelled": "Відмінується трасування шляху",
|
||||
"scanner_enableBluetooth": "Увімкніть Bluetooth",
|
||||
"scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.",
|
||||
"scanner_chromeRequired": "Потрібен браузер Chrome",
|
||||
"scanner_chromeRequiredMessage": "Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.",
|
||||
"scanner_bluetoothOff": "Bluetooth вимкнено",
|
||||
"snrIndicator_lastSeen": "Останній раз бачили",
|
||||
"snrIndicator_nearByRepeaters": "Ближні ретранслятори",
|
||||
@@ -1802,123 +1799,5 @@
|
||||
"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": "Встановити моє місцезнаходження",
|
||||
"settings_privacySubtitle": "Керуйте інформацією, яку буде спільно використовуватися",
|
||||
"settings_privacy": "Налаштування приватності",
|
||||
"settings_telemetryBaseMode": "Режим базової телеметрії",
|
||||
"settings_telemetryLocationMode": "Режим місця телеметрії",
|
||||
"settings_advertLocation": "Розміщення реклами",
|
||||
"settings_advertLocationSubtitle": "Включити місце розташування в оголошення",
|
||||
"settings_privacySettingsDescription": "Виберіть, яку інформацію ваш пристрій буде передавати іншим.",
|
||||
"settings_allowAll": "Дозволити все",
|
||||
"settings_denyAll": "Відхилити все",
|
||||
"settings_allowByContact": "Дозволити за контактними прапорцями",
|
||||
"settings_telemetryEnvironmentMode": "Режим середовища телеметрії",
|
||||
"contact_info": "Контактна інформація",
|
||||
"contact_teleBaseSubtitle": "Дозволити спільний доступ до рівня заряду батареї та базової телеметрії",
|
||||
"contact_teleLoc": "Розташування телеметрії",
|
||||
"contact_teleBase": "Базовий телебачення",
|
||||
"contact_teleLocSubtitle": "Дозволити спільне використання даних про місцеположення",
|
||||
"contact_settings": "Налаштування контактів",
|
||||
"contact_telemetry": "Телеметрія",
|
||||
"contact_clearChat": "Очистити чат",
|
||||
"contact_lastSeen": "Останній раз бачили",
|
||||
"contact_teleEnv": "Середовище телеметрії",
|
||||
"contact_teleEnvSubtitle": "Дозволити спільний доступ до даних датчиків середовища",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_telemetryModeUpdated": "Режим телеметрії оновлено",
|
||||
"settings_multiAck": "Багатократне підтвердження: {value}"
|
||||
"contacts_unread": "Непрочитане"
|
||||
}
|
||||
|
||||
+1
-122
@@ -300,7 +300,6 @@
|
||||
"contacts_newGroup": "新建群聊",
|
||||
"contacts_groupName": "群聊名称",
|
||||
"contacts_groupNameRequired": "请输入群聊名称",
|
||||
"contacts_groupNameReserved": "该群组名称已被保留",
|
||||
"contacts_groupAlreadyExists": "名为 \"{name}\" 的群聊已存在",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
"placeholders": {
|
||||
@@ -1610,8 +1609,6 @@
|
||||
"map_removeLast": "移除最后一个",
|
||||
"map_runTrace": "运行路径追踪",
|
||||
"scanner_bluetoothOffMessage": "请开启蓝牙以搜索设备",
|
||||
"scanner_chromeRequired": "需要 Chrome 浏览器",
|
||||
"scanner_chromeRequiredMessage": "此 Web 应用程序需要 Google Chrome 或基于 Chromium 的浏览器以支持蓝牙。",
|
||||
"scanner_bluetoothOff": "蓝牙已关闭",
|
||||
"scanner_enableBluetooth": "启用蓝牙",
|
||||
"snrIndicator_lastSeen": "最近访问",
|
||||
@@ -1807,123 +1804,5 @@
|
||||
"contacts_searchRepeaters": "搜索 {number}{str} 重复器...",
|
||||
"contacts_searchContactsNoNumber": "搜索联系人...",
|
||||
"contacts_searchRoomServers": "搜索 {number}{str} 房间服务器...",
|
||||
"contacts_searchFavorites": "搜索 {number}{str} 收藏...",
|
||||
"settings_contactSettings": "联系人设置",
|
||||
"contactsSettings_title": "联系人设置",
|
||||
"contactsSettings_autoAddUsersTitle": "自动添加用户",
|
||||
"contactsSettings_otherTitle": "其他联系人相关设置",
|
||||
"contactsSettings_autoAddUsersSubtitle": "允许伴侣自动添加发现的用户",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "允许伴侣自动添加发现的重复器",
|
||||
"contactsSettings_autoAddSensorsTitle": "自动添加传感器",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "允许伴侣自动添加发现的房间服务器",
|
||||
"contactsSettings_autoAddRepeatersTitle": "自动添加重复器",
|
||||
"contactsSettings_autoAddTitle": "自动发现",
|
||||
"settings_contactSettingsSubtitle": "添加联系人的设置",
|
||||
"contactsSettings_overwriteOldestTitle": "覆盖最旧的",
|
||||
"contactsSettings_autoAddSensorsSubtitle": "允许伴侣自动添加发现的传感器",
|
||||
"discoveredContacts_searchHint": "搜索已发现的联系人",
|
||||
"contactsSettings_autoAddRoomServersTitle": "自动添加房间服务器",
|
||||
"discoveredContacts_contactAdded": "联系人已添加",
|
||||
"discoveredContacts_deleteContact": "删除联系人",
|
||||
"discoveredContacts_addContact": "添加联系人",
|
||||
"discoveredContacts_noMatching": "没有匹配的联系人",
|
||||
"discoveredContacts_Title": "已发现的联系人",
|
||||
"discoveredContacts_copyContact": "复制联系人到剪贴板",
|
||||
"contactsSettings_overwriteOldestSubtitle": "当联系人列表已满时,将替换最老的非收藏联系人。",
|
||||
"common_deleteAll": "删除全部",
|
||||
"discoveredContacts_deleteContactAllContent": "您确定要删除所有发现的联系人吗?",
|
||||
"discoveredContacts_deleteContactAll": "删除所有发现的联系人",
|
||||
"map_showGuessedLocations": "显示猜测的节点位置",
|
||||
"map_guessedLocation": "猜测的位置",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"usbScreenTitle": "通过USB连接",
|
||||
"usbScreenSubtitle": "选择已检测到的串行设备,并直接连接到您的 MeshCore 节点。",
|
||||
"connectionChoiceBluetoothLabel": "蓝牙",
|
||||
"usbScreenStatus": "选择一个 USB 设备",
|
||||
"usbScreenNote": "USB 串行接口在支持的 Android 设备和桌面平台上处于活动状态。",
|
||||
"usbScreenEmptyState": "未找到任何 USB 设备。请插入一个,然后刷新。",
|
||||
"usbErrorPermissionDenied": "拒绝了USB权限。",
|
||||
"usbErrorDeviceMissing": "所选的USB设备已不再可用。",
|
||||
"usbErrorInvalidPort": "选择一个有效的USB设备。",
|
||||
"usbErrorBusy": "还有一个 USB 连接请求正在进行中。",
|
||||
"usbErrorNotConnected": "没有连接任何USB设备。",
|
||||
"usbErrorOpenFailed": "未能打开所选的USB设备。",
|
||||
"usbErrorConnectFailed": "未能连接到所选的USB设备。",
|
||||
"usbErrorUnsupported": "此平台不支持USB串行通信。",
|
||||
"usbErrorAlreadyActive": "USB 连接已建立。",
|
||||
"usbErrorNoDeviceSelected": "未选择任何 USB 设备。",
|
||||
"usbErrorPortClosed": "USB 连接未建立。",
|
||||
"usbFallbackDeviceName": "Web 串流设备",
|
||||
"@usbConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"usbStatus_searching": "正在搜索 USB 设备...",
|
||||
"usbStatus_connecting": "连接USB设备...",
|
||||
"usbStatus_notConnected": "选择一个 USB 设备",
|
||||
"usbConnectionFailed": "USB 连接失败:{error}",
|
||||
"usbErrorConnectTimedOut": "连接超时。请确保设备已安装 USB 伴侣固件。",
|
||||
"@tcpStatus_connectingTo": {
|
||||
"placeholders": {
|
||||
"endpoint": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@tcpConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tcpHostLabel": "IP地址",
|
||||
"tcpHostHint": "192.168.40.10",
|
||||
"tcpScreenTitle": "通过 TCP 连接",
|
||||
"connectionChoiceTcpLabel": "TCP",
|
||||
"tcpPortLabel": "端口",
|
||||
"tcpPortHint": "5000",
|
||||
"tcpStatus_notConnected": "输入目标地址,然后连接",
|
||||
"tcpStatus_connectingTo": "连接到 {endpoint}...",
|
||||
"tcpErrorHostRequired": "需要提供IP地址。",
|
||||
"tcpErrorPortInvalid": "端口号必须在 1 到 65535 之间。",
|
||||
"tcpErrorUnsupported": "此平台不支持 TCP 传输。",
|
||||
"tcpErrorTimedOut": "TCP 连接超时。",
|
||||
"tcpConnectionFailed": "TCP 连接失败:{error}",
|
||||
"map_showDiscoveryContacts": "显示发现联系人",
|
||||
"map_setAsMyLocation": "设置为我的位置",
|
||||
"settings_privacySubtitle": "控制要共享的信息。",
|
||||
"settings_privacySettingsDescription": "选择您的设备与他人共享的信息。",
|
||||
"settings_telemetryBaseMode": "遥测基础模式",
|
||||
"settings_telemetryLocationMode": "遥测位置模式",
|
||||
"settings_advertLocation": "广告位置",
|
||||
"settings_advertLocationSubtitle": "在广告中包含位置",
|
||||
"settings_allowByContact": "按联系人标志允许",
|
||||
"settings_denyAll": "拒绝所有",
|
||||
"settings_privacy": "隐私设置",
|
||||
"settings_allowAll": "允许全部",
|
||||
"contact_info": "联系信息",
|
||||
"contact_teleBase": "遥测基站",
|
||||
"contact_teleBaseSubtitle": "允许共享电池电量和基本遥测数据",
|
||||
"settings_telemetryEnvironmentMode": "遥测环境模式",
|
||||
"contact_teleLoc": "遥测位置",
|
||||
"contact_teleEnv": "遥测环境",
|
||||
"contact_teleEnvSubtitle": "允许共享环境传感器数据",
|
||||
"contact_clearChat": "清除聊天记录",
|
||||
"contact_lastSeen": "最近出现",
|
||||
"contact_settings": "联系人设置",
|
||||
"contact_teleLocSubtitle": "允许共享位置数据",
|
||||
"contact_telemetry": "遥测数据",
|
||||
"@settings_multiAck": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_multiAck": "多重ACK:{value}",
|
||||
"settings_telemetryModeUpdated": "遥测模式已更新"
|
||||
"contacts_searchFavorites": "搜索 {number}{str} 收藏..."
|
||||
}
|
||||
|
||||
+1
-21
@@ -4,9 +4,6 @@ import 'package:flutter/foundation.dart';
|
||||
import 'l10n/app_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'screens/chrome_required_screen.dart';
|
||||
import 'utils/platform_info.dart';
|
||||
|
||||
import 'connector/meshcore_connector.dart';
|
||||
import 'screens/scanner_screen.dart';
|
||||
import 'services/storage_service.dart';
|
||||
@@ -19,8 +16,6 @@ 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/ui_view_state_service.dart';
|
||||
import 'services/timeout_prediction_service.dart';
|
||||
import 'storage/prefs_manager.dart';
|
||||
import 'utils/app_logger.dart';
|
||||
|
||||
@@ -41,8 +36,6 @@ void main() async {
|
||||
final backgroundService = BackgroundService();
|
||||
final mapTileCacheService = MapTileCacheService();
|
||||
final chatTextScaleService = ChatTextScaleService();
|
||||
final uiViewStateService = UiViewStateService();
|
||||
final timeoutPredictionService = TimeoutPredictionService(storage);
|
||||
|
||||
// Load settings
|
||||
await appSettingsService.loadSettings();
|
||||
@@ -60,8 +53,6 @@ void main() async {
|
||||
_registerThirdPartyLicenses();
|
||||
|
||||
await chatTextScaleService.initialize();
|
||||
await uiViewStateService.initialize();
|
||||
await timeoutPredictionService.initialize();
|
||||
|
||||
// Wire up connector with services
|
||||
connector.initialize(
|
||||
@@ -71,7 +62,6 @@ void main() async {
|
||||
bleDebugLogService: bleDebugLogService,
|
||||
appDebugLogService: appDebugLogService,
|
||||
backgroundService: backgroundService,
|
||||
timeoutPredictionService: timeoutPredictionService,
|
||||
);
|
||||
|
||||
await connector.loadContactCache();
|
||||
@@ -93,8 +83,6 @@ void main() async {
|
||||
appDebugLogService: appDebugLogService,
|
||||
mapTileCacheService: mapTileCacheService,
|
||||
chatTextScaleService: chatTextScaleService,
|
||||
uiViewStateService: uiViewStateService,
|
||||
timeoutPredictionService: timeoutPredictionService,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -130,8 +118,6 @@ class MeshCoreApp extends StatelessWidget {
|
||||
final AppDebugLogService appDebugLogService;
|
||||
final MapTileCacheService mapTileCacheService;
|
||||
final ChatTextScaleService chatTextScaleService;
|
||||
final UiViewStateService uiViewStateService;
|
||||
final TimeoutPredictionService timeoutPredictionService;
|
||||
|
||||
const MeshCoreApp({
|
||||
super.key,
|
||||
@@ -144,8 +130,6 @@ class MeshCoreApp extends StatelessWidget {
|
||||
required this.appDebugLogService,
|
||||
required this.mapTileCacheService,
|
||||
required this.chatTextScaleService,
|
||||
required this.uiViewStateService,
|
||||
required this.timeoutPredictionService,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -159,10 +143,8 @@ class MeshCoreApp extends StatelessWidget {
|
||||
ChangeNotifierProvider.value(value: bleDebugLogService),
|
||||
ChangeNotifierProvider.value(value: appDebugLogService),
|
||||
ChangeNotifierProvider.value(value: chatTextScaleService),
|
||||
ChangeNotifierProvider.value(value: uiViewStateService),
|
||||
Provider.value(value: storage),
|
||||
Provider.value(value: mapTileCacheService),
|
||||
ChangeNotifierProvider.value(value: timeoutPredictionService),
|
||||
],
|
||||
child: Consumer<AppSettingsService>(
|
||||
builder: (context, settingsService, child) {
|
||||
@@ -205,9 +187,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(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -22,7 +22,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;
|
||||
@@ -39,9 +38,6 @@ class AppSettings {
|
||||
final Map<String, String> batteryChemistryByRepeaterId;
|
||||
final UnitSystem unitSystem;
|
||||
final Set<String> mutedChannels;
|
||||
final bool mapShowDiscoveryContacts;
|
||||
final String tcpServerAddress;
|
||||
final int tcpServerPort;
|
||||
|
||||
AppSettings({
|
||||
this.clearPathOnMaxRetry = false,
|
||||
@@ -52,7 +48,6 @@ class AppSettings {
|
||||
this.mapKeyPrefixEnabled = false,
|
||||
this.mapKeyPrefix = '',
|
||||
this.mapShowMarkers = true,
|
||||
this.mapShowGuessedLocations = true,
|
||||
this.enableMessageTracing = false,
|
||||
this.mapCacheBounds,
|
||||
this.mapCacheMinZoom = 10,
|
||||
@@ -69,9 +64,6 @@ class AppSettings {
|
||||
Map<String, String>? batteryChemistryByRepeaterId,
|
||||
this.unitSystem = UnitSystem.metric,
|
||||
Set<String>? mutedChannels,
|
||||
this.mapShowDiscoveryContacts = true,
|
||||
this.tcpServerAddress = '',
|
||||
this.tcpServerPort = 0,
|
||||
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
|
||||
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
|
||||
mutedChannels = mutedChannels ?? {};
|
||||
@@ -86,7 +78,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,
|
||||
@@ -103,9 +94,6 @@ class AppSettings {
|
||||
'battery_chemistry_by_repeater_id': batteryChemistryByRepeaterId,
|
||||
'unit_system': unitSystem.value,
|
||||
'muted_channels': mutedChannels.toList(),
|
||||
'map_show_discovery_contacts': mapShowDiscoveryContacts,
|
||||
'tcp_server_address': tcpServerAddress,
|
||||
'tcp_server_port': tcpServerPort,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -127,8 +115,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()),
|
||||
@@ -161,10 +147,6 @@ class AppSettings {
|
||||
?.map((e) => e.toString())
|
||||
.toSet()) ??
|
||||
{},
|
||||
mapShowDiscoveryContacts:
|
||||
json['map_show_discovery_contacts'] as bool? ?? true,
|
||||
tcpServerAddress: json['tcp_server_address'] as String? ?? '',
|
||||
tcpServerPort: json['tcp_server_port'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -177,7 +159,6 @@ class AppSettings {
|
||||
bool? mapKeyPrefixEnabled,
|
||||
String? mapKeyPrefix,
|
||||
bool? mapShowMarkers,
|
||||
bool? mapShowGuessedLocations,
|
||||
bool? enableMessageTracing,
|
||||
Object? mapCacheBounds = _unset,
|
||||
int? mapCacheMinZoom,
|
||||
@@ -194,9 +175,6 @@ class AppSettings {
|
||||
Map<String, String>? batteryChemistryByRepeaterId,
|
||||
UnitSystem? unitSystem,
|
||||
Set<String>? mutedChannels,
|
||||
bool? mapShowDiscoveryContacts,
|
||||
String? tcpServerAddress,
|
||||
int? tcpServerPort,
|
||||
}) {
|
||||
return AppSettings(
|
||||
clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry,
|
||||
@@ -207,8 +185,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
|
||||
@@ -233,10 +209,6 @@ class AppSettings {
|
||||
batteryChemistryByRepeaterId ?? this.batteryChemistryByRepeaterId,
|
||||
unitSystem: unitSystem ?? this.unitSystem,
|
||||
mutedChannels: mutedChannels ?? this.mutedChannels,
|
||||
mapShowDiscoveryContacts:
|
||||
mapShowDiscoveryContacts ?? this.mapShowDiscoveryContacts,
|
||||
tcpServerAddress: tcpServerAddress ?? this.tcpServerAddress,
|
||||
tcpServerPort: tcpServerPort ?? this.tcpServerPort,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+58
-47
@@ -1,6 +1,4 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:meshcore_open/utils/app_logger.dart';
|
||||
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
|
||||
class Contact {
|
||||
@@ -17,8 +15,6 @@ class Contact {
|
||||
final double? longitude;
|
||||
final DateTime lastSeen;
|
||||
final DateTime lastMessageAt;
|
||||
final bool isActive;
|
||||
final Uint8List? rawPacket;
|
||||
|
||||
Contact({
|
||||
required this.publicKey,
|
||||
@@ -33,8 +29,6 @@ class Contact {
|
||||
this.longitude,
|
||||
required this.lastSeen,
|
||||
DateTime? lastMessageAt,
|
||||
this.isActive = true,
|
||||
this.rawPacket,
|
||||
}) : lastMessageAt = lastMessageAt ?? lastSeen;
|
||||
|
||||
String get publicKeyHex => pubKeyToHex(publicKey);
|
||||
@@ -65,17 +59,7 @@ class Contact {
|
||||
return '$pathLength hops';
|
||||
}
|
||||
|
||||
bool get hasLocation {
|
||||
const double epsilon = 1e-6;
|
||||
final lat = latitude ?? 0.0;
|
||||
final lon = longitude ?? 0.0;
|
||||
return (lat.abs() > epsilon || lon.abs() > epsilon) &&
|
||||
lat >= -90.0 &&
|
||||
lat <= 90.0 &&
|
||||
lon >= -180.0 &&
|
||||
lon <= 180.0;
|
||||
}
|
||||
|
||||
bool get hasLocation => latitude != null && longitude != null;
|
||||
bool get isFavorite => (flags & contactFlagFavorite) != 0;
|
||||
|
||||
Contact copyWith({
|
||||
@@ -92,8 +76,6 @@ class Contact {
|
||||
double? longitude,
|
||||
DateTime? lastSeen,
|
||||
DateTime? lastMessageAt,
|
||||
bool? isActive,
|
||||
Uint8List? rawPacket,
|
||||
}) {
|
||||
return Contact(
|
||||
publicKey: publicKey ?? this.publicKey,
|
||||
@@ -112,13 +94,11 @@ class Contact {
|
||||
longitude: longitude ?? this.longitude,
|
||||
lastSeen: lastSeen ?? this.lastSeen,
|
||||
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
|
||||
isActive: isActive ?? this.isActive,
|
||||
rawPacket: rawPacket ?? this.rawPacket,
|
||||
);
|
||||
}
|
||||
|
||||
String get pathIdList {
|
||||
final pathBytes = pathBytesForDisplay;
|
||||
final pathBytes = _pathBytesForDisplay;
|
||||
if (pathBytes.isEmpty) return '';
|
||||
final parts = <String>[];
|
||||
final groupSize = pathHashSize;
|
||||
@@ -140,7 +120,43 @@ class Contact {
|
||||
return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>";
|
||||
}
|
||||
|
||||
Uint8List get pathBytesForDisplay {
|
||||
Uint8List? get traceRouteBytes {
|
||||
final pathBytes = _pathBytesForDisplay;
|
||||
Uint8List? traceBytes;
|
||||
|
||||
if (pathBytes.isEmpty) {
|
||||
traceBytes = Uint8List(1);
|
||||
traceBytes[0] = publicKey[0];
|
||||
return traceBytes;
|
||||
}
|
||||
|
||||
if (type == advTypeRepeater || type == advTypeRoom) {
|
||||
final len = (pathBytes.length + pathBytes.length + 1);
|
||||
traceBytes = Uint8List(len);
|
||||
traceBytes[pathBytes.length] = publicKey[0];
|
||||
for (int i = 0; i < pathBytes.length; i++) {
|
||||
traceBytes[i] = pathBytes[i];
|
||||
if (i < pathBytes.length) {
|
||||
traceBytes[len - 1 - i] = pathBytes[i];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (pathBytes.length < 2) {
|
||||
return pathBytes[0] == 0 ? null : pathBytes;
|
||||
}
|
||||
final len = (pathBytes.length + pathBytes.length - 1);
|
||||
traceBytes = Uint8List(len);
|
||||
for (int i = 0; i < pathBytes.length; i++) {
|
||||
traceBytes[i] = pathBytes[i];
|
||||
if (i < pathBytes.length - 1) {
|
||||
traceBytes[len - 1 - i] = pathBytes[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
return traceBytes;
|
||||
}
|
||||
|
||||
Uint8List get _pathBytesForDisplay {
|
||||
if (pathOverride != null) {
|
||||
if (pathOverride! < 0) return Uint8List(0);
|
||||
return pathOverrideBytes ?? Uint8List(0);
|
||||
@@ -150,28 +166,28 @@ class Contact {
|
||||
|
||||
static Contact? fromFrame(Uint8List data) {
|
||||
if (data.isEmpty) return null;
|
||||
final reader = BufferReader(data);
|
||||
if (data[0] != respCodeContact) return null;
|
||||
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 pubKey = Uint8List.fromList(
|
||||
data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize),
|
||||
);
|
||||
final type = data[contactTypeOffset];
|
||||
final flags = data[contactFlagsOffset];
|
||||
final pathLen = data[contactPathLenOffset].toSigned(8);
|
||||
final safePathLen = pathLen > 0
|
||||
? (pathLen > maxPathSize ? maxPathSize : pathLen)
|
||||
: 0;
|
||||
final pathBytes = reader.readBytes(maxPathSize).sublist(0, safePathLen);
|
||||
final name = reader.readCStringGreedy(maxNameSize);
|
||||
|
||||
final lastMod = reader.readUInt32LE();
|
||||
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();
|
||||
|
||||
final latRaw = readInt32LE(data, contactLatOffset);
|
||||
final lonRaw = readInt32LE(data, contactLonOffset);
|
||||
if (latRaw != 0 || lonRaw != 0) {
|
||||
lat = latRaw / 1e6;
|
||||
lon = lonRaw / 1e6;
|
||||
@@ -182,16 +198,14 @@ class Contact {
|
||||
name: name.isEmpty ? 'Unknown' : name,
|
||||
type: type,
|
||||
flags: flags,
|
||||
pathLength: pathLen > 0 ? (pathLen > maxPathSize ? -1 : pathLen) : -1,
|
||||
pathLength: pathLen,
|
||||
path: pathBytes,
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastMod * 1000),
|
||||
isActive: true,
|
||||
rawPacket: null,
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000),
|
||||
);
|
||||
} catch (e) {
|
||||
appLogger.error('Failed to parse contact frame: $e');
|
||||
// If parsing fails, return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -202,7 +216,4 @@ class Contact {
|
||||
|
||||
@override
|
||||
int get hashCode => publicKeyHex.hashCode;
|
||||
bool get teleBaseEnabled => (flags & contactFlagTeleBase) != 0;
|
||||
bool get teleLocEnabled => (flags & contactFlagTeleLoc) != 0;
|
||||
bool get teleEnvEnabled => (flags & contactFlagTeleEnv) != 0;
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -118,19 +118,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(''),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -166,33 +166,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
],
|
||||
),
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onSelected: (value) {
|
||||
if (value == 'clearChat') {
|
||||
context.read<MeshCoreConnector>().clearMessagesForChannel(
|
||||
widget.channel.index,
|
||||
);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'clearChat',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.delete, size: 20, color: Colors.red),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
context.l10n.contact_clearChat,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
|
||||
@@ -40,8 +40,8 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
final primaryPath = !channelMessage && !message.isOutgoing
|
||||
? Uint8List.fromList(primaryPathTmp.reversed.toList())
|
||||
: primaryPathTmp;
|
||||
final contacts = connector.allContacts;
|
||||
final hops = _buildPathHops(primaryPath, contacts, l10n);
|
||||
|
||||
final hops = _buildPathHops(primaryPath, connector.contacts, l10n);
|
||||
final hasHopDetails = primaryPath.isNotEmpty;
|
||||
final observedLabel = _formatObservedHops(
|
||||
primaryPath.length,
|
||||
@@ -62,9 +62,8 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
builder: (context) => PathTraceMapScreen(
|
||||
title: context.l10n.contacts_repeaterPathTrace,
|
||||
path: primaryPath,
|
||||
flipPathAround: true,
|
||||
reversePathAround:
|
||||
!(!channelMessage && !message.isOutgoing),
|
||||
flipPathRound: true,
|
||||
reversePathRound: !message.isOutgoing && !channelMessage,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -365,8 +364,11 @@ class _ChannelMessagePathMapScreenState
|
||||
: selectedPathTmp;
|
||||
|
||||
final selectedIndex = _indexForPath(selectedPath, observedPaths);
|
||||
final contacts = connector.allContacts;
|
||||
final hops = _buildPathHops(selectedPath, contacts, context.l10n);
|
||||
final hops = _buildPathHops(
|
||||
selectedPath,
|
||||
connector.contacts,
|
||||
context.l10n,
|
||||
);
|
||||
|
||||
final points = <LatLng>[];
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import 'package:uuid/uuid.dart';
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/ui_view_state_service.dart';
|
||||
import '../models/channel.dart';
|
||||
import '../models/community.dart';
|
||||
import '../storage/community_store.dart';
|
||||
@@ -29,6 +28,8 @@ import 'contacts_screen.dart';
|
||||
import 'map_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
|
||||
enum ChannelSortOption { manual, name, latestMessages, unread }
|
||||
|
||||
class ChannelsScreen extends StatefulWidget {
|
||||
final bool hideBackButton;
|
||||
|
||||
@@ -42,20 +43,17 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
with DisconnectNavigationMixin {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final CommunityStore _communityStore = CommunityStore();
|
||||
String _searchQuery = '';
|
||||
Timer? _searchDebounce;
|
||||
ChannelSortOption _sortOption = ChannelSortOption.manual;
|
||||
List<Community> _communities = [];
|
||||
|
||||
// Cache of PSK hex -> Community for quick lookup
|
||||
final Map<String, Community> _pskToCommunity = {};
|
||||
|
||||
ChannelMessageStore get _channelMessageStore => ChannelMessageStore();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController.text = context
|
||||
.read<UiViewStateService>()
|
||||
.channelsSearchText;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<MeshCoreConnector>().getChannels();
|
||||
_loadCommunities();
|
||||
@@ -63,8 +61,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(() {
|
||||
@@ -110,10 +106,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
final viewState = context.watch<UiViewStateService>();
|
||||
|
||||
final channelMessageStore = ChannelMessageStore();
|
||||
channelMessageStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||
|
||||
// Auto-navigate back to scanner if disconnected
|
||||
if (!checkConnectionAndNavigate(connector)) {
|
||||
@@ -206,7 +199,6 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
final filteredChannels = _filterAndSortChannels(
|
||||
channels,
|
||||
connector,
|
||||
viewState,
|
||||
);
|
||||
|
||||
return Column(
|
||||
@@ -221,19 +213,17 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
suffixIcon: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (viewState.channelsSearchText.isNotEmpty)
|
||||
if (_searchQuery.isNotEmpty)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_searchDebounce?.cancel();
|
||||
_searchDebounce = null;
|
||||
_searchController.clear();
|
||||
context
|
||||
.read<UiViewStateService>()
|
||||
.setChannelsSearchText('');
|
||||
setState(() {
|
||||
_searchQuery = '';
|
||||
});
|
||||
},
|
||||
),
|
||||
_buildFilterButton(viewState),
|
||||
_buildFilterButton(),
|
||||
],
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
@@ -250,9 +240,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
const Duration(milliseconds: 300),
|
||||
() {
|
||||
if (!mounted) return;
|
||||
context
|
||||
.read<UiViewStateService>()
|
||||
.setChannelsSearchText(value);
|
||||
setState(() {
|
||||
_searchQuery = value.toLowerCase();
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -287,9 +277,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
),
|
||||
],
|
||||
)
|
||||
: (viewState.channelsSortOption ==
|
||||
ChannelSortOption.manual &&
|
||||
viewState.channelsSearchText.isEmpty)
|
||||
: (_sortOption == ChannelSortOption.manual &&
|
||||
_searchQuery.isEmpty)
|
||||
? ReorderableListView.builder(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
@@ -589,40 +578,59 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
await showDisconnectDialog(context, connector);
|
||||
}
|
||||
|
||||
Widget _buildFilterButton(UiViewStateService viewState) {
|
||||
return SortFilterMenu<ChannelSortOption>(
|
||||
Widget _buildFilterButton() {
|
||||
const actionSortManual = 0;
|
||||
const actionSortName = 1;
|
||||
const actionSortLatest = 2;
|
||||
const actionSortUnread = 3;
|
||||
|
||||
return SortFilterMenu(
|
||||
tooltip: context.l10n.listFilter_tooltip,
|
||||
sections: [
|
||||
SortFilterMenuSection<ChannelSortOption>(
|
||||
SortFilterMenuSection(
|
||||
title: context.l10n.channels_sortBy,
|
||||
options: [
|
||||
SortFilterMenuOption<ChannelSortOption>(
|
||||
value: ChannelSortOption.manual,
|
||||
SortFilterMenuOption(
|
||||
value: actionSortManual,
|
||||
label: context.l10n.channels_sortManual,
|
||||
checked: viewState.channelsSortOption == ChannelSortOption.manual,
|
||||
checked: _sortOption == ChannelSortOption.manual,
|
||||
),
|
||||
SortFilterMenuOption<ChannelSortOption>(
|
||||
value: ChannelSortOption.name,
|
||||
SortFilterMenuOption(
|
||||
value: actionSortName,
|
||||
label: context.l10n.channels_sortAZ,
|
||||
checked: viewState.channelsSortOption == ChannelSortOption.name,
|
||||
checked: _sortOption == ChannelSortOption.name,
|
||||
),
|
||||
SortFilterMenuOption<ChannelSortOption>(
|
||||
value: ChannelSortOption.latestMessages,
|
||||
SortFilterMenuOption(
|
||||
value: actionSortLatest,
|
||||
label: context.l10n.channels_sortLatestMessages,
|
||||
checked:
|
||||
viewState.channelsSortOption ==
|
||||
ChannelSortOption.latestMessages,
|
||||
checked: _sortOption == ChannelSortOption.latestMessages,
|
||||
),
|
||||
SortFilterMenuOption<ChannelSortOption>(
|
||||
value: ChannelSortOption.unread,
|
||||
SortFilterMenuOption(
|
||||
value: actionSortUnread,
|
||||
label: context.l10n.channels_sortUnread,
|
||||
checked: viewState.channelsSortOption == ChannelSortOption.unread,
|
||||
checked: _sortOption == ChannelSortOption.unread,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
onSelected: (sortOption) {
|
||||
viewState.setChannelsSortOption(sortOption);
|
||||
onSelected: (action) {
|
||||
setState(() {
|
||||
switch (action) {
|
||||
case actionSortManual:
|
||||
_sortOption = ChannelSortOption.manual;
|
||||
break;
|
||||
case actionSortLatest:
|
||||
_sortOption = ChannelSortOption.latestMessages;
|
||||
break;
|
||||
case actionSortUnread:
|
||||
_sortOption = ChannelSortOption.unread;
|
||||
break;
|
||||
case actionSortName:
|
||||
default:
|
||||
_sortOption = ChannelSortOption.name;
|
||||
break;
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -630,14 +638,11 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
List<Channel> _filterAndSortChannels(
|
||||
List<Channel> channels,
|
||||
MeshCoreConnector connector,
|
||||
UiViewStateService viewState,
|
||||
) {
|
||||
var filtered = channels.where((channel) {
|
||||
if (viewState.channelsSearchText.isEmpty) return true;
|
||||
if (_searchQuery.isEmpty) return true;
|
||||
final label = _normalizeChannelName(channel);
|
||||
return label.toLowerCase().contains(
|
||||
viewState.channelsSearchText.toLowerCase(),
|
||||
);
|
||||
return label.toLowerCase().contains(_searchQuery);
|
||||
}).toList();
|
||||
|
||||
int compareByName(Channel a, Channel b) {
|
||||
@@ -646,7 +651,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
return nameA.toLowerCase().compareTo(nameB.toLowerCase());
|
||||
}
|
||||
|
||||
switch (viewState.channelsSortOption) {
|
||||
switch (_sortOption) {
|
||||
case ChannelSortOption.manual:
|
||||
break;
|
||||
case ChannelSortOption.latestMessages:
|
||||
@@ -707,8 +712,6 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
bool isRegularHashtag = true;
|
||||
Community? selectedCommunity;
|
||||
|
||||
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
@@ -760,9 +763,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
);
|
||||
}
|
||||
|
||||
Widget? buildExpandedContent(
|
||||
ChannelMessageStore channelMessageStore,
|
||||
) {
|
||||
Widget? buildExpandedContent() {
|
||||
switch (selectedOption) {
|
||||
case 0: // Create Private Channel
|
||||
return Column(
|
||||
@@ -787,7 +788,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
children: [
|
||||
Expanded(
|
||||
child: FilledButton(
|
||||
onPressed: () async {
|
||||
onPressed: () {
|
||||
final name = nameController.text.trim();
|
||||
if (name.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
@@ -809,14 +810,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(
|
||||
@@ -1335,8 +1329,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,
|
||||
@@ -1345,8 +1338,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(
|
||||
@@ -1356,8 +1348,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(
|
||||
@@ -1367,8 +1358,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,
|
||||
@@ -1376,8 +1366,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,
|
||||
@@ -1385,8 +1374,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()!,
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1536,7 +1524,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
try {
|
||||
await connector.deleteChannel(channel.index);
|
||||
|
||||
await channelMessageStore.clearChannelMessages(channel.index);
|
||||
channelMessageStore.clearChannelMessages(channel.index);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
@@ -1761,7 +1749,6 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
}
|
||||
|
||||
final channelCount = communityChannels.length;
|
||||
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
|
||||
+84
-297
@@ -36,7 +36,6 @@ import '../widgets/gif_picker.dart';
|
||||
import '../widgets/path_selection_dialog.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import 'telemetry_screen.dart';
|
||||
|
||||
class ChatScreen extends StatefulWidget {
|
||||
final Contact contact;
|
||||
@@ -107,9 +106,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
final unreadLabel = context.l10n.chat_unread(unreadCount);
|
||||
final pathLabel = _currentPathLabel(contact);
|
||||
|
||||
// Show path details if we have non-empty path data (from device or override)
|
||||
// Show path details if we have path data (from device or override)
|
||||
final hasPathData =
|
||||
contact.path.isNotEmpty || contact.pathOverrideBytes != null;
|
||||
final effectivePath = contact.pathOverrideBytes ?? contact.path;
|
||||
final hasPathData = effectivePath.isNotEmpty;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -143,25 +143,12 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
final contact = _resolveContact(connector);
|
||||
final isFloodMode = contact.pathOverride == -1;
|
||||
|
||||
final isDirectMode = contact.pathOverride == 0;
|
||||
final activeMode = isFloodMode
|
||||
? 'flood'
|
||||
: isDirectMode
|
||||
? 'direct'
|
||||
: 'auto';
|
||||
|
||||
return PopupMenuButton<String>(
|
||||
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
|
||||
tooltip: context.l10n.chat_routingMode,
|
||||
onSelected: (mode) async {
|
||||
if (mode == 'flood') {
|
||||
await connector.setPathOverride(contact, pathLen: -1);
|
||||
} else if (mode == 'direct') {
|
||||
await connector.setPathOverride(
|
||||
contact,
|
||||
pathLen: 0,
|
||||
pathBytes: Uint8List(0),
|
||||
);
|
||||
} else {
|
||||
await connector.setPathOverride(contact, pathLen: null);
|
||||
}
|
||||
@@ -174,7 +161,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
Icon(
|
||||
Icons.auto_mode,
|
||||
size: 20,
|
||||
color: activeMode == 'auto'
|
||||
color: !isFloodMode
|
||||
? Theme.of(context).primaryColor
|
||||
: null,
|
||||
),
|
||||
@@ -182,30 +169,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
Text(
|
||||
context.l10n.chat_autoUseSavedPath,
|
||||
style: TextStyle(
|
||||
fontWeight: activeMode == 'auto'
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'direct',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.near_me,
|
||||
size: 20,
|
||||
color: activeMode == 'direct'
|
||||
? Theme.of(context).primaryColor
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
context.l10n.chat_direct,
|
||||
style: TextStyle(
|
||||
fontWeight: activeMode == 'direct'
|
||||
fontWeight: !isFloodMode
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
@@ -220,7 +184,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
Icon(
|
||||
Icons.waves,
|
||||
size: 20,
|
||||
color: activeMode == 'flood'
|
||||
color: isFloodMode
|
||||
? Theme.of(context).primaryColor
|
||||
: null,
|
||||
),
|
||||
@@ -228,7 +192,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
Text(
|
||||
context.l10n.chat_forceFloodMode,
|
||||
style: TextStyle(
|
||||
fontWeight: activeMode == 'flood'
|
||||
fontWeight: isFloodMode
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
@@ -245,77 +209,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
tooltip: context.l10n.chat_pathManagement,
|
||||
onPressed: () => _showPathHistory(context),
|
||||
),
|
||||
Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, _) {
|
||||
return PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onSelected: (value) {
|
||||
if (value == 'info') {
|
||||
_showContactInfo(context);
|
||||
}
|
||||
if (value == 'settings') {
|
||||
_showContactSettings(context);
|
||||
}
|
||||
if (value == 'telemetry') {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
TelemetryScreen(contact: widget.contact),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (value == 'clearChat') {
|
||||
connector.clearMessagesForContact(widget.contact);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'info',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.info_outline, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Text(context.l10n.contact_info),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'telemetry',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.bar_chart, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Text(context.l10n.contact_telemetry),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'settings',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.settings, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Text(context.l10n.contact_settings),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'clearChat',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.delete, size: 20, color: Colors.red),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
context.l10n.contact_clearChat,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
IconButton(
|
||||
icon: const Icon(Icons.info_outline),
|
||||
onPressed: () => _showContactInfo(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -355,9 +251,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
context.l10n.chat_sendMessageTo(
|
||||
_resolveContact(context.read<MeshCoreConnector>()).name,
|
||||
),
|
||||
context.l10n.chat_sendMessageTo(widget.contact.name),
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
|
||||
),
|
||||
],
|
||||
@@ -375,7 +269,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
|
||||
// Auto-scroll to bottom if user is already at bottom
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
_scrollController.scrollToBottomIfAtBottom();
|
||||
});
|
||||
|
||||
@@ -400,10 +293,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
);
|
||||
}
|
||||
final messageIndex = index;
|
||||
Contact contact = _resolveContact(connector);
|
||||
Contact contact = widget.contact;
|
||||
final message = reversedMessages[messageIndex];
|
||||
String fourByteHex = '';
|
||||
if (contact.type == advTypeRoom) {
|
||||
if (widget.contact.type == advTypeRoom) {
|
||||
contact = _resolveContactFrom4Bytes(
|
||||
connector,
|
||||
message.fourByteRoomContactKey.isEmpty
|
||||
@@ -421,13 +314,12 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
final textScale = context.select<ChatTextScaleService, double>(
|
||||
(service) => service.scale,
|
||||
);
|
||||
final resolvedContact = _resolveContact(connector);
|
||||
return _MessageBubble(
|
||||
message: message,
|
||||
senderName: resolvedContact.type == advTypeRoom
|
||||
senderName: widget.contact.type == advTypeRoom
|
||||
? "${contact.name} [$fourByteHex]"
|
||||
: contact.name,
|
||||
isRoomServer: resolvedContact.type == advTypeRoom,
|
||||
isRoomServer: widget.contact.type == advTypeRoom,
|
||||
textScale: textScale,
|
||||
onTap: () => _openMessagePath(message, contact),
|
||||
onLongPress: () => _showMessageActions(message, contact),
|
||||
@@ -565,7 +457,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
return;
|
||||
}
|
||||
|
||||
connector.sendMessage(_resolveContact(connector), text);
|
||||
connector.sendMessage(widget.contact, text);
|
||||
_textController.clear();
|
||||
_textFieldFocusNode.requestFocus();
|
||||
}
|
||||
@@ -762,7 +654,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
|
||||
// Set the path override to persist user's choice
|
||||
await connector.setPathOverride(
|
||||
_resolveContact(connector),
|
||||
widget.contact,
|
||||
pathLen: pathLength,
|
||||
pathBytes: pathBytes,
|
||||
);
|
||||
@@ -771,7 +663,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
Navigator.pop(context);
|
||||
await _notifyPathSet(
|
||||
connector,
|
||||
_resolveContact(connector),
|
||||
widget.contact,
|
||||
pathBytes,
|
||||
path.hopCount,
|
||||
);
|
||||
@@ -830,9 +722,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
style: const TextStyle(fontSize: 11),
|
||||
),
|
||||
onTap: () async {
|
||||
await connector.clearContactPath(
|
||||
_resolveContact(connector),
|
||||
);
|
||||
await connector.clearContactPath(widget.contact);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
@@ -860,7 +750,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
),
|
||||
onTap: () async {
|
||||
await connector.setPathOverride(
|
||||
_resolveContact(connector),
|
||||
widget.contact,
|
||||
pathLen: -1,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
@@ -927,8 +817,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
builder: (context) => PathTraceMapScreen(
|
||||
title: context.l10n.contacts_repeaterPathTrace,
|
||||
path: Uint8List.fromList(pathBytes),
|
||||
flipPathAround: true,
|
||||
targetContact: widget.contact,
|
||||
flipPathRound: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -943,22 +832,11 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
int _resolveContactIndex = -1;
|
||||
|
||||
Contact _resolveContact(MeshCoreConnector connector) {
|
||||
if (_resolveContactIndex >= 0 &&
|
||||
_resolveContactIndex < connector.contacts.length &&
|
||||
connector.contacts[_resolveContactIndex].publicKeyHex ==
|
||||
widget.contact.publicKeyHex) {
|
||||
return connector.contacts[_resolveContactIndex];
|
||||
}
|
||||
_resolveContactIndex = connector.contacts.indexWhere(
|
||||
return connector.contacts.firstWhere(
|
||||
(c) => c.publicKeyHex == widget.contact.publicKeyHex,
|
||||
orElse: () => widget.contact,
|
||||
);
|
||||
if (_resolveContactIndex == -1) {
|
||||
return widget.contact;
|
||||
}
|
||||
return connector.contacts[_resolveContactIndex];
|
||||
}
|
||||
|
||||
Contact _resolveContactFrom4Bytes(
|
||||
@@ -1010,128 +888,60 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
|
||||
void _showContactInfo(BuildContext context) {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
final contact = _resolveContact(connector);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: SelectableText(contact.name),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow(context.l10n.chat_type, contact.typeLabel),
|
||||
_buildInfoRow(context.l10n.chat_path, contact.pathLabel),
|
||||
_buildInfoRow(
|
||||
context.l10n.contact_lastSeen,
|
||||
_formatContactLastMessage(contact.lastMessageAt),
|
||||
),
|
||||
if (contact.hasLocation)
|
||||
_buildInfoRow(
|
||||
context.l10n.chat_location,
|
||||
'${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}',
|
||||
),
|
||||
_buildInfoRow(context.l10n.chat_publicKey, contact.publicKeyHex),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(context.l10n.common_close),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showContactSettings(BuildContext context) {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
connector.ensureContactSmazSettingLoaded(widget.contact.publicKeyHex);
|
||||
final contact = widget.contact;
|
||||
bool smazEnabled = connector.isContactSmazEnabled(contact.publicKeyHex);
|
||||
bool teleBaseEnabled = contact.teleBaseEnabled;
|
||||
bool teleLocEnabled = contact.teleLocEnabled;
|
||||
bool teleEnvEnabled = contact.teleEnvEnabled;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setDialogState) => AlertDialog(
|
||||
title: Text(context.l10n.contact_settings),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (contact.hasLocation) ...[
|
||||
builder: (context) => Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, _) {
|
||||
final contact = _resolveContact(connector);
|
||||
final smazEnabled = connector.isContactSmazEnabled(
|
||||
contact.publicKeyHex,
|
||||
);
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(contact.name),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow(context.l10n.chat_type, contact.typeLabel),
|
||||
_buildInfoRow(context.l10n.chat_path, contact.pathLabel),
|
||||
if (contact.hasLocation)
|
||||
_buildInfoRow(
|
||||
context.l10n.chat_location,
|
||||
'${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}',
|
||||
),
|
||||
_buildInfoRow(
|
||||
context.l10n.chat_location,
|
||||
'${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}',
|
||||
context.l10n.chat_publicKey,
|
||||
'${contact.publicKeyHex.substring(0, 16)}...',
|
||||
),
|
||||
const Divider(),
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(context.l10n.channels_smazCompression),
|
||||
subtitle: Text(context.l10n.chat_compressOutgoingMessages),
|
||||
value: smazEnabled,
|
||||
onChanged: (value) {
|
||||
connector.setContactSmazEnabled(
|
||||
contact.publicKeyHex,
|
||||
value,
|
||||
);
|
||||
},
|
||||
),
|
||||
const Divider(height: 8),
|
||||
],
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(context.l10n.channels_smazCompression),
|
||||
subtitle: Text(context.l10n.chat_compressOutgoingMessages),
|
||||
value: smazEnabled,
|
||||
onChanged: (value) {
|
||||
connector.setContactSmazEnabled(
|
||||
contact.publicKeyHex,
|
||||
value,
|
||||
);
|
||||
setDialogState(() => smazEnabled = value);
|
||||
},
|
||||
),
|
||||
const Divider(height: 8),
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(context.l10n.contact_teleBase),
|
||||
subtitle: Text(context.l10n.contact_teleBaseSubtitle),
|
||||
value: teleBaseEnabled,
|
||||
onChanged: (value) {
|
||||
setDialogState(() => teleBaseEnabled = value);
|
||||
},
|
||||
),
|
||||
const Divider(height: 8),
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(context.l10n.contact_teleLoc),
|
||||
subtitle: Text(context.l10n.contact_teleLocSubtitle),
|
||||
value: teleLocEnabled,
|
||||
onChanged: (value) {
|
||||
setDialogState(() => teleLocEnabled = value);
|
||||
},
|
||||
),
|
||||
const Divider(height: 8),
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(context.l10n.contact_teleEnv),
|
||||
subtitle: Text(context.l10n.contact_teleEnvSubtitle),
|
||||
value: teleEnvEnabled,
|
||||
onChanged: (value) {
|
||||
setDialogState(() => teleEnvEnabled = value);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
connector.setContactFlags(
|
||||
contact,
|
||||
teleBase: teleBaseEnabled,
|
||||
teleLoc: teleLocEnabled,
|
||||
teleEnv: teleEnvEnabled,
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(context.l10n.common_close),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(context.l10n.common_close),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1146,32 +956,12 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
width: 80,
|
||||
child: Text(label, style: TextStyle(color: Colors.grey[600])),
|
||||
),
|
||||
Expanded(child: SelectableText(value)),
|
||||
Expanded(child: Text(value)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatContactLastMessage(DateTime timestamp) {
|
||||
final diff = DateTime.now().difference(timestamp);
|
||||
if (diff.isNegative || diff.inMinutes < 5) {
|
||||
return context.l10n.contacts_lastSeenNow;
|
||||
}
|
||||
if (diff.inMinutes < 60) {
|
||||
return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes);
|
||||
}
|
||||
if (diff.inHours < 24) {
|
||||
final hours = diff.inHours;
|
||||
return hours == 1
|
||||
? context.l10n.contacts_lastSeenHourAgo
|
||||
: context.l10n.contacts_lastSeenHoursAgo(hours);
|
||||
}
|
||||
final days = diff.inDays;
|
||||
return days == 1
|
||||
? context.l10n.contacts_lastSeenDayAgo
|
||||
: context.l10n.contacts_lastSeenDaysAgo(days);
|
||||
}
|
||||
|
||||
void _openChat(BuildContext context, Contact contact) {
|
||||
// Check if this is a repeater
|
||||
context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex);
|
||||
@@ -1195,7 +985,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
final currentPathLabel = _currentPathLabel(currentContact);
|
||||
|
||||
// Filter out the current contact from available contacts
|
||||
final availableContacts = connector.allContacts
|
||||
final availableContacts = connector.contacts
|
||||
.where((c) => c != widget.contact)
|
||||
.toList();
|
||||
|
||||
@@ -1214,7 +1004,11 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
);
|
||||
|
||||
if (result == null) {
|
||||
return; // Cancelled — keep existing path
|
||||
appLogger.info(
|
||||
'PathSelectionDialog was cancelled or returned null',
|
||||
tag: 'ChatScreen',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mounted) {
|
||||
@@ -1230,19 +1024,14 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
tag: 'ChatScreen',
|
||||
);
|
||||
await connector.setPathOverride(
|
||||
_resolveContact(connector),
|
||||
widget.contact,
|
||||
pathLen: result.length,
|
||||
pathBytes: result,
|
||||
);
|
||||
appLogger.info('setPathOverride completed', tag: 'ChatScreen');
|
||||
|
||||
if (!mounted) return;
|
||||
await _notifyPathSet(
|
||||
connector,
|
||||
_resolveContact(connector),
|
||||
result,
|
||||
result.length,
|
||||
);
|
||||
await _notifyPathSet(connector, widget.contact, result, result.length);
|
||||
}
|
||||
|
||||
void _openMessagePath(Message message, Contact contact) {
|
||||
@@ -1254,10 +1043,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
final String senderName;
|
||||
if (message.isOutgoing) {
|
||||
senderName = connector.selfName ?? context.l10n.chat_me;
|
||||
} else if (_resolveContact(connector).type == advTypeRoom) {
|
||||
} else if (widget.contact.type == advTypeRoom) {
|
||||
senderName = "${contact.name} [$fourByteHex]";
|
||||
} else {
|
||||
senderName = _resolveContact(connector).name;
|
||||
senderName = widget.contact.name;
|
||||
}
|
||||
final pathMessage = ChannelMessage(
|
||||
senderKey: null,
|
||||
@@ -1320,8 +1109,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
_retryMessage(message);
|
||||
},
|
||||
),
|
||||
if (_resolveContact(context.read<MeshCoreConnector>()).type ==
|
||||
advTypeRoom)
|
||||
if (widget.contact.type == advTypeRoom)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.chat),
|
||||
title: Text(context.l10n.contacts_openChat),
|
||||
@@ -1359,7 +1147,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
void _retryMessage(Message message) {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
// Retry using the contact's current path override setting
|
||||
connector.sendMessage(_resolveContact(connector), message.text);
|
||||
connector.sendMessage(widget.contact, message.text);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(context.l10n.chat_retryingMessage)));
|
||||
@@ -1385,8 +1173,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
|
||||
// For room servers, include sender name (like channels) since multiple users
|
||||
// For 1:1 chats, sender is implicit (null)
|
||||
final liveContact = _resolveContact(connector);
|
||||
final senderName = liveContact.type == advTypeRoom
|
||||
final senderName = widget.contact.type == advTypeRoom
|
||||
? senderContact.name
|
||||
: null;
|
||||
final hash = ReactionHelper.computeReactionHash(
|
||||
@@ -1395,7 +1182,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
message.text,
|
||||
);
|
||||
final reactionText = 'r:$hash:$emojiIndex';
|
||||
connector.sendMessage(_resolveContact(connector), reactionText);
|
||||
connector.sendMessage(widget.contact, reactionText);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -51,9 +51,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 +209,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
|
||||
|
||||
+289
-505
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
+11
-375
@@ -1,7 +1,6 @@
|
||||
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';
|
||||
@@ -16,7 +15,6 @@ 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';
|
||||
@@ -51,8 +49,7 @@ class MapScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MapScreenState extends State<MapScreen> {
|
||||
// Zoom level at which node labels start to appear
|
||||
static const double _labelZoomThreshold = 12.0;
|
||||
static const double _labelZoomThreshold = 8.5;
|
||||
|
||||
final MapController _mapController = MapController();
|
||||
final MapMarkerService _markerService = MapMarkerService();
|
||||
@@ -67,8 +64,6 @@ class _MapScreenState extends State<MapScreen> {
|
||||
final List<Polyline> _polylines = [];
|
||||
bool _legendExpanded = false;
|
||||
bool _showNodeLabels = true;
|
||||
List<_GuessedLocation> _cachedGuessedLocations = [];
|
||||
String _guessedLocationsCacheKey = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -93,15 +88,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;
|
||||
@@ -133,16 +119,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 = connector.allContacts;
|
||||
|
||||
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)
|
||||
@@ -175,44 +156,10 @@ class _MapScreenState extends State<MapScreen> {
|
||||
: filteredByTime;
|
||||
|
||||
// Filter by location
|
||||
final contactsWithLocation = filteredByKeyPrefix.where((c) {
|
||||
return c.hasLocation;
|
||||
}).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
|
||||
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
|
||||
@@ -483,11 +430,6 @@ class _MapScreenState extends State<MapScreen> {
|
||||
size: 34,
|
||||
),
|
||||
),
|
||||
if (!_isBuildingPathTrace)
|
||||
..._buildGuessedMarker(
|
||||
guessedLocations,
|
||||
showLabels: _showNodeLabels,
|
||||
),
|
||||
..._buildMarkers(
|
||||
contactsWithLocation,
|
||||
settings,
|
||||
@@ -547,7 +489,6 @@ class _MapScreenState extends State<MapScreen> {
|
||||
contactsWithLocation,
|
||||
settings,
|
||||
sharedMarkers.length,
|
||||
guessedLocations.length,
|
||||
),
|
||||
if (_isBuildingPathTrace) _buildPathTraceOverlay(),
|
||||
],
|
||||
@@ -571,234 +512,6 @@ 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, {
|
||||
@@ -944,7 +657,6 @@ class _MapScreenState extends State<MapScreen> {
|
||||
List<Contact> contactsWithLocation,
|
||||
settings,
|
||||
int markerCount,
|
||||
int guessedCount,
|
||||
) {
|
||||
int nodeCount = 0;
|
||||
for (final contact in contactsWithLocation) {
|
||||
@@ -984,12 +696,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,
|
||||
@@ -1057,12 +764,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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1251,12 +952,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(
|
||||
@@ -1276,16 +972,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),
|
||||
@@ -1302,9 +992,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,
|
||||
@@ -1318,9 +1005,6 @@ class _MapScreenState extends State<MapScreen> {
|
||||
if (contact.type == advTypeRepeater)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (!contact.isActive) {
|
||||
connector.importDiscoveredContact(contact);
|
||||
}
|
||||
Navigator.pop(dialogContext);
|
||||
_showRepeaterLogin(context, contact);
|
||||
},
|
||||
@@ -1329,9 +1013,6 @@ class _MapScreenState extends State<MapScreen> {
|
||||
if (contact.type == advTypeRoom)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (!contact.isActive) {
|
||||
connector.importDiscoveredContact(contact);
|
||||
}
|
||||
Navigator.pop(dialogContext);
|
||||
_showRoomLogin(context, contact);
|
||||
},
|
||||
@@ -1499,23 +1180,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),
|
||||
@@ -1817,22 +1481,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,
|
||||
@@ -2096,18 +1744,6 @@ class _MapScreenState extends State<MapScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
class _GuessedLocation {
|
||||
final Contact contact;
|
||||
final LatLng position;
|
||||
final bool highConfidence;
|
||||
|
||||
_GuessedLocation({
|
||||
required this.contact,
|
||||
required this.position,
|
||||
required this.highConfidence,
|
||||
});
|
||||
}
|
||||
|
||||
class _MarkerPayload {
|
||||
final LatLng position;
|
||||
final String label;
|
||||
|
||||
@@ -44,24 +44,6 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||
PathSelection? _pendingStatusSelection;
|
||||
List<Map<String, dynamic>>? _parsedNeighbors;
|
||||
|
||||
int _resolveRepeaterIndex = -1;
|
||||
|
||||
Contact _resolveRepeater(MeshCoreConnector connector) {
|
||||
if (_resolveRepeaterIndex >= 0 &&
|
||||
_resolveRepeaterIndex < connector.contacts.length &&
|
||||
connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
|
||||
widget.repeater.publicKeyHex) {
|
||||
return connector.contacts[_resolveRepeaterIndex];
|
||||
}
|
||||
_resolveRepeaterIndex = connector.contacts.indexWhere(
|
||||
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
|
||||
);
|
||||
if (_resolveRepeaterIndex == -1) {
|
||||
return widget.repeater;
|
||||
}
|
||||
return connector.contacts[_resolveRepeaterIndex];
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -142,11 +124,12 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||
|
||||
void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) {
|
||||
final buffer = BufferReader(frame);
|
||||
final contacts = connector.allContacts;
|
||||
try {
|
||||
final neighborCount = buffer.readUInt16LE();
|
||||
final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE());
|
||||
contacts.where((c) => c.type == advTypeRepeater).forEach((repeater) {
|
||||
connector.contacts.where((c) => c.type == advTypeRepeater).forEach((
|
||||
repeater,
|
||||
) {
|
||||
for (var neighborData in parsedNeighbors) {
|
||||
final publicKey = neighborData['publicKey'];
|
||||
if (listEquals(
|
||||
@@ -181,6 +164,13 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Contact _resolveRepeater(MeshCoreConnector connector) {
|
||||
return connector.contacts.firstWhere(
|
||||
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
|
||||
orElse: () => widget.repeater,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadNeighbors() async {
|
||||
if (_commandService == null) return;
|
||||
|
||||
|
||||
+37
-234
@@ -52,18 +52,16 @@ class PathTraceMapScreen extends StatefulWidget {
|
||||
final String title;
|
||||
final Uint8List path;
|
||||
final int? repeaterId;
|
||||
final bool flipPathAround;
|
||||
final bool reversePathAround;
|
||||
final Contact? targetContact;
|
||||
final bool flipPathRound;
|
||||
final bool reversePathRound;
|
||||
|
||||
const PathTraceMapScreen({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.path,
|
||||
this.repeaterId,
|
||||
this.flipPathAround = false,
|
||||
this.reversePathAround = false,
|
||||
this.targetContact,
|
||||
this.flipPathRound = false,
|
||||
this.reversePathRound = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -80,11 +78,6 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
bool _failed2Loaded = false;
|
||||
bool _hasData = false;
|
||||
PathTraceData? _traceData;
|
||||
// Inferred positions for hops that have no GPS location, keyed by hop byte.
|
||||
Map<int, LatLng> _inferredHopPositions = {};
|
||||
// Endpoint position for the target contact (GPS or guessed).
|
||||
LatLng? _targetContactPosition;
|
||||
bool _targetContactIsGuessed = false;
|
||||
List<LatLng> _points = <LatLng>[];
|
||||
List<Polyline> _polylines = [];
|
||||
LatLng? _initialCenter = LatLng(0, 0);
|
||||
@@ -93,7 +86,6 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
ValueKey<String> _mapKey = const ValueKey('initial');
|
||||
double _pathDistanceMeters = 0.0;
|
||||
bool _showNodeLabels = true;
|
||||
Contact? _targetContact;
|
||||
|
||||
String _formatPathPrefixes(Uint8List pathBytes) {
|
||||
return pathBytes
|
||||
@@ -115,37 +107,14 @@ 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;
|
||||
@@ -159,17 +128,17 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
final pathTmp = widget.reversePathAround
|
||||
final Uint8List path;
|
||||
|
||||
Uint8List pathTmp = widget.reversePathRound
|
||||
? Uint8List.fromList(widget.path.reversed.toList())
|
||||
: widget.path;
|
||||
|
||||
final path = widget.flipPathAround ? buildPath(pathTmp) : pathTmp;
|
||||
|
||||
appLogger.info(
|
||||
'Initiating path trace with path: ${_formatPathPrefixes(path)}',
|
||||
tag: 'PathTraceMapScreen',
|
||||
noNotify: !mounted,
|
||||
);
|
||||
if (widget.flipPathRound) {
|
||||
path = addReturnPath(pathTmp);
|
||||
} else {
|
||||
path = pathTmp;
|
||||
}
|
||||
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
final frame = buildTraceReq(
|
||||
@@ -259,8 +228,10 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
.toList();
|
||||
|
||||
Map<int, Contact> pathContacts = {};
|
||||
final contacts = connector.allContacts;
|
||||
contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
|
||||
|
||||
connector.contacts.where((c) => c.type != advTypeChat).forEach((
|
||||
repeater,
|
||||
) {
|
||||
for (var repeaterData in pathData) {
|
||||
if (listEquals(
|
||||
repeater.publicKey.sublist(0, 1),
|
||||
@@ -271,125 +242,23 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
}
|
||||
});
|
||||
|
||||
// For hops with no GPS contact, infer position from other contacts
|
||||
// with known GPS that share the same last-hop byte.
|
||||
final Map<int, LatLng> inferredPositions = {};
|
||||
for (final hop in pathData) {
|
||||
final contact = pathContacts[hop];
|
||||
if (contact != null && contact.hasLocation) continue;
|
||||
final peers = connector.contacts
|
||||
.where(
|
||||
(c) => c.hasLocation && c.path.isNotEmpty && c.path.last == hop,
|
||||
)
|
||||
.toList();
|
||||
if (peers.isNotEmpty) {
|
||||
final lat =
|
||||
peers.map((c) => c.latitude!).reduce((a, b) => a + b) /
|
||||
peers.length;
|
||||
final lon =
|
||||
peers.map((c) => c.longitude!).reduce((a, b) => a + b) /
|
||||
peers.length;
|
||||
inferredPositions[hop] = LatLng(lat, lon);
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_hasData = true;
|
||||
_inferredHopPositions = inferredPositions;
|
||||
_traceData = PathTraceData(
|
||||
pathData: pathData,
|
||||
snrData: snrData,
|
||||
pathContacts: pathContacts,
|
||||
);
|
||||
// Compute endpoint position for the target contact.
|
||||
LatLng? targetPos;
|
||||
bool targetGuessed = false;
|
||||
_targetContact = widget.targetContact;
|
||||
|
||||
if (_targetContact != null) {
|
||||
final tc = _targetContact!;
|
||||
if (tc.hasLocation) {
|
||||
targetPos = LatLng(tc.latitude!, tc.longitude!);
|
||||
} else if (widget.path.length > 1) {
|
||||
// Infer from the last hop: average GPS contacts sharing that hop.
|
||||
// For a round-trip path (flipPathAround/reversePathAround), the target-side hop
|
||||
// sits in the middle of the symmetric sequence; .last is the local side.
|
||||
final lastHop = widget.reversePathAround
|
||||
? widget.path.first
|
||||
: widget.path.last;
|
||||
|
||||
final peers = connector.allContacts
|
||||
.where(
|
||||
(c) =>
|
||||
c.hasLocation &&
|
||||
c.path.isNotEmpty &&
|
||||
c.path.last == lastHop,
|
||||
)
|
||||
.toList();
|
||||
if (peers.isNotEmpty) {
|
||||
final lat =
|
||||
peers.map((c) => c.latitude!).reduce((a, b) => a + b) /
|
||||
peers.length;
|
||||
final lon =
|
||||
peers.map((c) => c.longitude!).reduce((a, b) => a + b) /
|
||||
peers.length;
|
||||
const offsetDeg = 0.003;
|
||||
final angle = (tc.publicKey[1] / 255.0) * 2 * pi;
|
||||
targetPos = LatLng(
|
||||
lat + offsetDeg * cos(angle),
|
||||
lon + offsetDeg * sin(angle),
|
||||
);
|
||||
targetGuessed = true;
|
||||
} else if (inferredPositions.containsKey(lastHop)) {
|
||||
final lat = inferredPositions[lastHop]!.latitude;
|
||||
final lon = inferredPositions[lastHop]!.longitude;
|
||||
const offsetDeg = 0.003;
|
||||
final angle = (tc.publicKey[1] / 255.0) * 2 * pi;
|
||||
targetPos = LatLng(
|
||||
lat + offsetDeg * cos(angle),
|
||||
lon + offsetDeg * sin(angle),
|
||||
);
|
||||
targetGuessed = true;
|
||||
} else {
|
||||
// As a last resort, just place it at the same position as the last hop.
|
||||
final contact = pathContacts[lastHop];
|
||||
if (contact != null && contact.hasLocation) {
|
||||
const offsetDeg = 0.003;
|
||||
final angle = (tc.publicKey[1] / 255.0) * 2 * pi;
|
||||
targetPos = LatLng(
|
||||
contact.latitude! + offsetDeg * cos(angle),
|
||||
contact.longitude! + offsetDeg * sin(angle),
|
||||
);
|
||||
targetGuessed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_targetContactPosition = targetPos;
|
||||
_targetContactIsGuessed = targetGuessed;
|
||||
|
||||
_points = <LatLng>[];
|
||||
_points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
||||
int hopLast = 0;
|
||||
int hopLastLast = 0;
|
||||
for (final hop in _traceData!.pathData) {
|
||||
if (hop == hopLastLast && widget.flipPathAround) {
|
||||
break; //skip duplicate hops in round-trip paths
|
||||
}
|
||||
final contact = _traceData!.pathContacts[hop];
|
||||
if (contact != null && contact.hasLocation) {
|
||||
if (contact != null &&
|
||||
contact.hasLocation &&
|
||||
contact.latitude != null &&
|
||||
contact.longitude != null) {
|
||||
_points.add(LatLng(contact.latitude!, contact.longitude!));
|
||||
} else {
|
||||
final inferred = inferredPositions[hop];
|
||||
if (inferred != null) _points.add(inferred);
|
||||
}
|
||||
hopLastLast = hopLast;
|
||||
hopLast = hop;
|
||||
}
|
||||
if (targetPos != null) {
|
||||
if (_targetContact != null && _targetContact!.type == advTypeChat) {
|
||||
_points.add(targetPos);
|
||||
}
|
||||
}
|
||||
_polylines = _points.length > 1
|
||||
@@ -480,8 +349,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_hasData)
|
||||
_buildMapPathTrace(context, tileCache, _targetContact),
|
||||
if (_hasData) _buildMapPathTrace(context, tileCache),
|
||||
if (_points.isEmpty &&
|
||||
!_hasData &&
|
||||
!_isLoading &&
|
||||
@@ -510,28 +378,12 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
List<Marker> _buildHopMarkers(
|
||||
List<int> pathData, {
|
||||
required bool showLabels,
|
||||
required Contact? target,
|
||||
}) {
|
||||
final markers = <Marker>[];
|
||||
int hopLast = 0;
|
||||
int hopLastLast = 0;
|
||||
for (final hop in pathData) {
|
||||
final contact = _traceData!.pathContacts[hop];
|
||||
final inferred = _inferredHopPositions[hop];
|
||||
final hasGps = contact != null && contact.hasLocation;
|
||||
if (hop == hopLastLast && widget.flipPathAround) {
|
||||
continue; //skip duplicate hops in round-trip paths
|
||||
}
|
||||
if (!hasGps && inferred == null) {
|
||||
hopLastLast = hopLast;
|
||||
hopLast = hop;
|
||||
continue; //skip hops with no GPS and no inferred position
|
||||
}
|
||||
final point = hasGps
|
||||
? LatLng(contact.latitude!, contact.longitude!)
|
||||
: inferred!;
|
||||
final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||
|
||||
if (contact == null || !contact.hasLocation) continue;
|
||||
final point = LatLng(contact.latitude!, contact.longitude!);
|
||||
markers.add(
|
||||
Marker(
|
||||
point: point,
|
||||
@@ -540,9 +392,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: hasGps
|
||||
? Colors.green
|
||||
: Colors.orange.withValues(alpha: 0.75),
|
||||
color: Colors.green,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
@@ -555,7 +405,10 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
hasGps ? label : '~$label',
|
||||
contact.publicKey
|
||||
.sublist(0, 1)
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||
.join(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -566,15 +419,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
),
|
||||
);
|
||||
if (showLabels) {
|
||||
markers.add(
|
||||
_buildNodeLabelMarker(
|
||||
point: point,
|
||||
label: contact?.name ?? '~$label',
|
||||
),
|
||||
);
|
||||
markers.add(_buildNodeLabelMarker(point: point, label: contact.name));
|
||||
}
|
||||
hopLastLast = hopLast;
|
||||
hopLast = hop;
|
||||
}
|
||||
|
||||
final selfLat = context.read<MeshCoreConnector>().selfLatitude;
|
||||
@@ -622,47 +468,6 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
// Add target contact endpoint marker.
|
||||
final targetPos = _targetContactPosition;
|
||||
if (targetPos != null && target != null && target.type == advTypeChat) {
|
||||
final isGuessed = _targetContactIsGuessed;
|
||||
final targetName = target.name;
|
||||
markers.add(
|
||||
Marker(
|
||||
point: targetPos,
|
||||
width: 35,
|
||||
height: 35,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: isGuessed
|
||||
? Colors.purple.withValues(alpha: 0.55)
|
||||
: Colors.red,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const Icon(Icons.person, color: Colors.white, size: 18),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (showLabels) {
|
||||
markers.add(
|
||||
_buildNodeLabelMarker(
|
||||
point: targetPos,
|
||||
label: isGuessed ? '~$targetName' : targetName,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return markers;
|
||||
}
|
||||
|
||||
@@ -762,7 +567,6 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
Widget _buildMapPathTrace(
|
||||
BuildContext context,
|
||||
MapTileCacheService tileCache,
|
||||
Contact? target,
|
||||
) {
|
||||
return FlutterMap(
|
||||
key: _mapKey,
|
||||
@@ -801,7 +605,6 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
markers: _buildHopMarkers(
|
||||
_traceData!.pathData,
|
||||
showLabels: _showNodeLabels,
|
||||
target: target,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -77,22 +77,11 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
int _resolveRepeaterIndex = -1;
|
||||
|
||||
Contact _resolveRepeater(MeshCoreConnector connector) {
|
||||
if (_resolveRepeaterIndex >= 0 &&
|
||||
_resolveRepeaterIndex < connector.contacts.length &&
|
||||
connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
|
||||
widget.repeater.publicKeyHex) {
|
||||
return connector.contacts[_resolveRepeaterIndex];
|
||||
}
|
||||
_resolveRepeaterIndex = connector.contacts.indexWhere(
|
||||
return connector.contacts.firstWhere(
|
||||
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
|
||||
orElse: () => widget.repeater,
|
||||
);
|
||||
if (_resolveRepeaterIndex == -1) {
|
||||
return widget.repeater;
|
||||
}
|
||||
return connector.contacts[_resolveRepeaterIndex];
|
||||
}
|
||||
|
||||
void _handleTextMessageResponse(Uint8List frame) {
|
||||
|
||||
@@ -205,7 +205,8 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TelemetryScreen(contact: repeater),
|
||||
builder: (context) =>
|
||||
TelemetryScreen(repeater: repeater, password: password),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -129,22 +129,11 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
_commandService?.handleResponse(widget.repeater, parsed.text);
|
||||
}
|
||||
|
||||
int _resolveRepeaterIndex = -1;
|
||||
|
||||
Contact _resolveRepeater(MeshCoreConnector connector) {
|
||||
if (_resolveRepeaterIndex >= 0 &&
|
||||
_resolveRepeaterIndex < connector.contacts.length &&
|
||||
connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
|
||||
widget.repeater.publicKeyHex) {
|
||||
return connector.contacts[_resolveRepeaterIndex];
|
||||
}
|
||||
_resolveRepeaterIndex = connector.contacts.indexWhere(
|
||||
return connector.contacts.firstWhere(
|
||||
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
|
||||
orElse: () => widget.repeater,
|
||||
);
|
||||
if (_resolveRepeaterIndex == -1) {
|
||||
return widget.repeater;
|
||||
}
|
||||
return connector.contacts[_resolveRepeaterIndex];
|
||||
}
|
||||
|
||||
bool _matchesRepeaterPrefix(Uint8List prefix) {
|
||||
|
||||
@@ -91,22 +91,11 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
int _resolveRepeaterIndex = -1;
|
||||
|
||||
Contact _resolveRepeater(MeshCoreConnector connector) {
|
||||
if (_resolveRepeaterIndex >= 0 &&
|
||||
_resolveRepeaterIndex < connector.contacts.length &&
|
||||
connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
|
||||
widget.repeater.publicKeyHex) {
|
||||
return connector.contacts[_resolveRepeaterIndex];
|
||||
}
|
||||
_resolveRepeaterIndex = connector.contacts.indexWhere(
|
||||
return connector.contacts.firstWhere(
|
||||
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
|
||||
orElse: () => widget.repeater,
|
||||
);
|
||||
if (_resolveRepeaterIndex == -1) {
|
||||
return widget.repeater;
|
||||
}
|
||||
return connector.contacts[_resolveRepeaterIndex];
|
||||
}
|
||||
|
||||
void _handleTextMessageResponse(Uint8List frame) {
|
||||
|
||||
+43
-116
@@ -1,17 +1,15 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../utils/platform_info.dart';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../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,7 +21,6 @@ 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;
|
||||
@@ -31,15 +28,12 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
@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,52 +44,33 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
}
|
||||
};
|
||||
|
||||
_connector.addListener(_connectionListener);
|
||||
connector.addListener(_connectionListener);
|
||||
|
||||
_bluetoothStateSubscription = FlutterBluePlus.adapterState.listen(
|
||||
(state) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_bluetoothState = state;
|
||||
});
|
||||
// Cancel scan if Bluetooth turns off while scanning
|
||||
if (state != BluetoothAdapterState.on) {
|
||||
unawaited(_connector.stopScan());
|
||||
}
|
||||
_bluetoothStateSubscription = FlutterBluePlus.adapterState.listen((state) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_bluetoothState = state;
|
||||
});
|
||||
// Cancel scan if Bluetooth turns off while scanning
|
||||
if (state != BluetoothAdapterState.on) {
|
||||
unawaited(connector.stopScan());
|
||||
}
|
||||
},
|
||||
onError: (Object e) {
|
||||
appLogger.warn('Adapter state stream error: $e', tag: 'ScannerScreen');
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_connector.removeListener(_connectionListener);
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
connector.removeListener(_connectionListener);
|
||||
unawaited(_bluetoothStateSubscription.cancel());
|
||||
if (!_changedNavigation) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
unawaited(_connector.disconnect(manual: true));
|
||||
});
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final canPop = Navigator.of(context).canPop();
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: canPop
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
appLogger.info('Back button pressed', tag: 'ScannerScreen');
|
||||
Navigator.of(context).maybePop();
|
||||
},
|
||||
)
|
||||
: null,
|
||||
title: AdaptiveAppBarTitle(context.l10n.scanner_title),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
@@ -120,84 +95,36 @@ 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: isBluetoothOff
|
||||
? null
|
||||
: () {
|
||||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -338,7 +265,7 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (PlatformInfo.isAndroid)
|
||||
if (Platform.isAndroid)
|
||||
TextButton(
|
||||
onPressed: () => FlutterBluePlus.turnOn(),
|
||||
child: Text(context.l10n.scanner_enableBluetooth),
|
||||
|
||||
@@ -8,7 +8,7 @@ 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 '../widgets/adaptive_app_bar_title.dart';
|
||||
import 'app_settings_screen.dart';
|
||||
import 'app_debug_log_screen.dart';
|
||||
import 'ble_debug_log_screen.dart';
|
||||
@@ -43,11 +43,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final l10n = context.l10n;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: AppBarTitle(
|
||||
l10n.settings_title,
|
||||
indicators: false,
|
||||
subtitle: false,
|
||||
),
|
||||
title: AdaptiveAppBarTitle(l10n.settings_title),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
@@ -277,20 +274,12 @@ 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_privacy),
|
||||
subtitle: Text(l10n.settings_privacySubtitle),
|
||||
title: Text(l10n.settings_privacyMode),
|
||||
subtitle: Text(l10n.settings_privacyModeSubtitle),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _privacySettings(context, connector),
|
||||
onTap: () => _togglePrivacy(context, connector),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -657,6 +646,47 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _togglePrivacy(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(l10n.settings_privacyMode),
|
||||
content: Text(l10n.settings_privacyModeToggle),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
await connector.setPrivacyMode(true);
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_privacyModeEnabled)),
|
||||
);
|
||||
},
|
||||
child: Text(l10n.common_enable),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
await connector.setPrivacyMode(false);
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_privacyModeDisabled)),
|
||||
);
|
||||
},
|
||||
child: Text(l10n.common_disable),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _sendAdvert(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
connector.sendSelfAdvert(flood: true);
|
||||
@@ -819,252 +849,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());
|
||||
}
|
||||
}
|
||||
|
||||
void _privacySettings(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
|
||||
int telemetryMode = connector.telemetryModeBase;
|
||||
int telemetryLocMode = connector.telemetryModeLoc;
|
||||
int telemetryEnvMode = connector.telemetryModeEnv;
|
||||
bool advertLocPolicy = connector.advertLocationPolicy == 0 ? false : true;
|
||||
int multiAcks = connector.multiAcks;
|
||||
|
||||
final telemModeBase = [
|
||||
DropdownMenuItem(value: teleModeDeny, child: Text(l10n.settings_denyAll)),
|
||||
DropdownMenuItem(
|
||||
value: teleModeAllowFlags,
|
||||
child: Text(l10n.settings_allowByContact),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: teleModeAllowAll,
|
||||
child: Text(l10n.settings_allowAll),
|
||||
),
|
||||
];
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
builder: (context, setDialogState) => AlertDialog(
|
||||
title: Text(l10n.settings_privacy),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.settings_privacySettingsDescription),
|
||||
const SizedBox(height: 16),
|
||||
FeatureToggleRow(
|
||||
title: l10n.settings_advertLocation,
|
||||
subtitle: l10n.settings_advertLocationSubtitle,
|
||||
value: advertLocPolicy,
|
||||
onChanged: (value) {
|
||||
setDialogState(() => advertLocPolicy = value);
|
||||
advertLocPolicy = value;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<int>(
|
||||
initialValue: telemetryMode,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.settings_telemetryBaseMode,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: telemModeBase,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setDialogState(() => telemetryMode = value);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<int>(
|
||||
initialValue: telemetryLocMode,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.settings_telemetryLocationMode,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: telemModeBase,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setDialogState(() => telemetryLocMode = value);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<int>(
|
||||
initialValue: telemetryEnvMode,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.settings_telemetryEnvironmentMode,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: telemModeBase,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setDialogState(() => telemetryEnvMode = value);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.settings_multiAck(multiAcks.toString()),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
Slider(
|
||||
value: multiAcks.toDouble(),
|
||||
min: 0,
|
||||
max: 2,
|
||||
divisions: 2,
|
||||
label: multiAcks.toString(),
|
||||
onChanged: (value) {
|
||||
setDialogState(() => multiAcks = value.round());
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
await connector.setTelemetryModeBase(
|
||||
telemetryMode,
|
||||
telemetryLocMode,
|
||||
telemetryEnvMode,
|
||||
advertLocPolicy ? 1 : 0,
|
||||
multiAcks,
|
||||
);
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_telemetryModeUpdated)),
|
||||
);
|
||||
},
|
||||
child: Text(l10n.common_save),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _RadioSettingsDialog extends StatefulWidget {
|
||||
|
||||
@@ -1,295 +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 '../services/app_settings_service.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(
|
||||
text: context.read<AppSettingsService>().settings.tcpServerAddress,
|
||||
);
|
||||
_portController = TextEditingController(
|
||||
text: context.read<AppSettingsService>().settings.tcpServerPort > 0
|
||||
? context.read<AppSettingsService>().settings.tcpServerPort.toString()
|
||||
: '',
|
||||
);
|
||||
_connector = context.read<MeshCoreConnector>();
|
||||
|
||||
_connectionListener = () {
|
||||
if (!mounted) return;
|
||||
if (_connector.state == MeshCoreConnectionState.disconnected) {
|
||||
_navigatedToContacts = false;
|
||||
}
|
||||
if (_connector.state == MeshCoreConnectionState.connected &&
|
||||
_connector.isTcpTransportConnected &&
|
||||
!_navigatedToContacts) {
|
||||
context.read<AppSettingsService>().setTcpServerAddress(
|
||||
_hostController.text,
|
||||
);
|
||||
context.read<AppSettingsService>().setTcpServerPort(
|
||||
int.tryParse(_portController.text) ?? 0,
|
||||
);
|
||||
_navigatedToContacts = true;
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const ContactsScreen()),
|
||||
);
|
||||
}
|
||||
};
|
||||
_connector.addListener(_connectionListener);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hostController.dispose();
|
||||
_portController.dispose();
|
||||
_connector.removeListener(_connectionListener);
|
||||
if (!_navigatedToContacts &&
|
||||
_connector.activeTransport == MeshCoreTransportType.tcp &&
|
||||
_connector.state != MeshCoreConnectionState.disconnected) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
unawaited(_connector.disconnect(manual: true));
|
||||
});
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
title: AdaptiveAppBarTitle(context.l10n.tcpScreenTitle),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, child) {
|
||||
final isConnecting =
|
||||
connector.state == MeshCoreConnectionState.connecting &&
|
||||
connector.activeTransport == MeshCoreTransportType.tcp;
|
||||
final isButtonDisabled =
|
||||
isConnecting ||
|
||||
connector.state == MeshCoreConnectionState.scanning;
|
||||
return Column(
|
||||
children: [
|
||||
_buildStatusBar(context, connector),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _hostController,
|
||||
decoration: InputDecoration(
|
||||
labelText: context.l10n.tcpHostLabel,
|
||||
hintText: context.l10n.tcpHostHint,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
enabled: !isConnecting,
|
||||
keyboardType: TextInputType.url,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _portController,
|
||||
decoration: InputDecoration(
|
||||
labelText: context.l10n.tcpPortLabel,
|
||||
hintText: context.l10n.tcpPortHint,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
enabled: !isConnecting,
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton.icon(
|
||||
key: const Key('tcp_connect_button'),
|
||||
onPressed: isButtonDisabled ? null : _connectTcp,
|
||||
icon: isConnecting
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.lan),
|
||||
label: Text(
|
||||
isConnecting
|
||||
? context.l10n.scanner_connecting
|
||||
: context.l10n.common_connect,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
top: false,
|
||||
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerRight,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (PlatformInfo.supportsUsbSerial)
|
||||
FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const UsbScreen()),
|
||||
);
|
||||
},
|
||||
heroTag: 'tcp_usb_action',
|
||||
extendedPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
icon: const Icon(Icons.usb),
|
||||
label: Text(context.l10n.connectionChoiceUsbLabel),
|
||||
),
|
||||
if (PlatformInfo.supportsUsbSerial) const SizedBox(width: 12),
|
||||
FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
Navigator.of(context).maybePop();
|
||||
},
|
||||
heroTag: 'tcp_ble_action',
|
||||
extendedPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
icon: const Icon(Icons.bluetooth),
|
||||
label: Text(context.l10n.connectionChoiceBluetoothLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusBar(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
String statusText;
|
||||
Color statusColor;
|
||||
|
||||
if (connector.isTcpTransportConnected) {
|
||||
statusText = l10n.scanner_connectedTo(
|
||||
connector.activeTcpEndpoint ?? 'TCP',
|
||||
);
|
||||
statusColor = Colors.green;
|
||||
} else if (connector.state == MeshCoreConnectionState.connecting &&
|
||||
connector.activeTransport == MeshCoreTransportType.tcp) {
|
||||
statusText = l10n.tcpStatus_connectingTo(
|
||||
'${_hostController.text}:${_portController.text}',
|
||||
);
|
||||
statusColor = Colors.orange;
|
||||
} else if (connector.state == MeshCoreConnectionState.disconnecting &&
|
||||
connector.activeTransport == MeshCoreTransportType.tcp) {
|
||||
statusText = l10n.scanner_disconnecting;
|
||||
statusColor = Colors.orange;
|
||||
} else {
|
||||
statusText = l10n.tcpStatus_notConnected;
|
||||
statusColor = Colors.grey;
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
color: statusColor.withValues(alpha: 0.1),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.circle, size: 12, color: statusColor),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _connectTcp() async {
|
||||
if (_connector.state == MeshCoreConnectionState.connecting ||
|
||||
_connector.state == MeshCoreConnectionState.connected ||
|
||||
_connector.state == MeshCoreConnectionState.disconnecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
final host = _hostController.text.trim();
|
||||
final parsedPort = int.tryParse(_portController.text.trim());
|
||||
if (host.isEmpty) {
|
||||
_showError(context.l10n.tcpErrorHostRequired);
|
||||
return;
|
||||
}
|
||||
if (parsedPort == null || parsedPort < 1 || parsedPort > 65535) {
|
||||
_showError(context.l10n.tcpErrorPortInvalid);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await _connector.connectTcp(host: host, port: parsedPort);
|
||||
} catch (error) {
|
||||
if (!mounted) return;
|
||||
_showError(_friendlyErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
void _showError(String message) {
|
||||
if (!mounted) return;
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -10,22 +10,30 @@ import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/repeater_command_service.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../widgets/path_management_dialog.dart';
|
||||
import '../helpers/cayenne_lpp.dart';
|
||||
import '../utils/battery_utils.dart';
|
||||
|
||||
class TelemetryScreen extends StatefulWidget {
|
||||
final Contact contact;
|
||||
final Contact repeater;
|
||||
final String password;
|
||||
|
||||
const TelemetryScreen({super.key, required this.contact});
|
||||
const TelemetryScreen({
|
||||
super.key,
|
||||
required this.repeater,
|
||||
required this.password,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TelemetryScreen> createState() => _TelemetryScreenState();
|
||||
}
|
||||
|
||||
class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
int _tagData = 0;
|
||||
static const int _statusPayloadOffset = 8;
|
||||
static const int _statusStatsSize = 52;
|
||||
static const int _statusResponseBytes =
|
||||
_statusPayloadOffset + _statusStatsSize;
|
||||
Uint8List _tagData = Uint8List(4);
|
||||
|
||||
bool _isLoading = false;
|
||||
bool _isLoaded = false;
|
||||
@@ -36,26 +44,6 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
PathSelection? _pendingStatusSelection;
|
||||
List<Map<String, dynamic>>? _parsedTelemetry;
|
||||
|
||||
int _tripTime = 0;
|
||||
|
||||
int _resolveContactIndex = -1;
|
||||
|
||||
Contact _resolveContact(MeshCoreConnector connector) {
|
||||
if (_resolveContactIndex >= 0 &&
|
||||
_resolveContactIndex < connector.contacts.length &&
|
||||
connector.contacts[_resolveContactIndex].publicKeyHex ==
|
||||
widget.contact.publicKeyHex) {
|
||||
return connector.contacts[_resolveContactIndex];
|
||||
}
|
||||
_resolveContactIndex = connector.contacts.indexWhere(
|
||||
(c) => c.publicKeyHex == widget.contact.publicKeyHex,
|
||||
);
|
||||
if (_resolveContactIndex == -1) {
|
||||
return widget.contact;
|
||||
}
|
||||
return connector.contacts[_resolveContactIndex];
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -72,62 +60,27 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
// Listen for incoming text messages from the repeater
|
||||
_frameSubscription = connector.receivedFrames.listen((frame) {
|
||||
if (frame.isEmpty) return;
|
||||
final reader = BufferReader(frame);
|
||||
try {
|
||||
final cmd = reader.readByte();
|
||||
if (cmd == respCodeSent) {
|
||||
reader.skipBytes(1); // Skip the reserved byte
|
||||
_tagData = reader.readUInt32LE();
|
||||
_tripTime = reader.readUInt32LE();
|
||||
_statusTimeout?.cancel();
|
||||
_statusTimeout = Timer(Duration(milliseconds: _tripTime), () {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_isLoaded = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.telemetry_requestTimeout),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
_recordTelemetryResult(false);
|
||||
});
|
||||
}
|
||||
|
||||
// Check if it's a binary response
|
||||
if (cmd == pushCodeBinaryResponse) {
|
||||
if (!mounted) return;
|
||||
reader.skipBytes(1); // Skip the reserved byte
|
||||
if (reader.readUInt32LE() != _tagData) return;
|
||||
_handleTelemetryResponse(reader.readRemainingBytes());
|
||||
}
|
||||
if (frame[0] == respCodeSent) {
|
||||
_tagData = frame.sublist(2, 6);
|
||||
}
|
||||
|
||||
// Check if it's a telemetry response (for chat contacts)
|
||||
if (cmd == pushCodeTelemetryResponse) {
|
||||
reader.skipBytes(1); // Skip the reserved byte
|
||||
final pubkey = reader.readBytes(6);
|
||||
if (!mounted) return;
|
||||
if (!listEquals(widget.contact.publicKey.sublist(0, 6), pubkey)) {
|
||||
return;
|
||||
}
|
||||
_handleTelemetryResponse(reader.readRemainingBytes());
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.error('Error parsing incoming frame: $e');
|
||||
// If parsing fails, ignore the frame
|
||||
// Check if it's a binary response
|
||||
if (frame[0] == pushCodeBinaryResponse &&
|
||||
listEquals(frame.sublist(2, 6), _tagData)) {
|
||||
if (!mounted) return;
|
||||
_handleStatusResponse(frame.sublist(6));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _handleTelemetryResponse(Uint8List frame) {
|
||||
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.contact.publicKeyHex,
|
||||
widget.repeater.publicKeyHex,
|
||||
batteryMv,
|
||||
source: 'telemetry',
|
||||
);
|
||||
@@ -152,6 +105,13 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
Contact _resolveRepeater(MeshCoreConnector connector) {
|
||||
return connector.contacts.firstWhere(
|
||||
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
|
||||
orElse: () => widget.repeater,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadTelemetry() async {
|
||||
if (_commandService == null) return;
|
||||
|
||||
@@ -161,20 +121,41 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
});
|
||||
try {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
final selection = await connector.preparePathForContactSend(
|
||||
_resolveContact(connector),
|
||||
);
|
||||
final repeater = _resolveRepeater(connector);
|
||||
final selection = await connector.preparePathForContactSend(repeater);
|
||||
_pendingStatusSelection = selection;
|
||||
Uint8List frame;
|
||||
if (widget.contact.type != advTypeChat) {
|
||||
frame = buildSendBinaryReq(
|
||||
widget.contact.publicKey,
|
||||
payload: Uint8List.fromList([reqTypeGetTelemetry]),
|
||||
);
|
||||
} else {
|
||||
frame = buildSendTelemetryReq(widget.contact.publicKey);
|
||||
}
|
||||
final frame = buildSendBinaryReq(
|
||||
repeater.publicKey,
|
||||
payload: Uint8List.fromList([reqTypeGetTelemetry]),
|
||||
);
|
||||
await connector.sendFrame(frame);
|
||||
|
||||
final pathLengthValue = selection.useFlood ? -1 : selection.hopCount;
|
||||
var messageBytes = frame.length >= _statusResponseBytes
|
||||
? frame.length
|
||||
: _statusResponseBytes;
|
||||
if (messageBytes < maxFrameSize) {
|
||||
messageBytes = maxFrameSize;
|
||||
}
|
||||
final timeoutMs = connector.calculateTimeout(
|
||||
pathLength: pathLengthValue,
|
||||
messageBytes: messageBytes,
|
||||
);
|
||||
_statusTimeout?.cancel();
|
||||
_statusTimeout = Timer(Duration(milliseconds: timeoutMs), () {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_isLoaded = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.telemetry_requestTimeout),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
_recordStatusResult(false);
|
||||
});
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@@ -192,16 +173,12 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
void _recordTelemetryResult(bool success) {
|
||||
void _recordStatusResult(bool success) {
|
||||
final selection = _pendingStatusSelection;
|
||||
if (selection == null) return;
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
connector.recordRepeaterPathResult(
|
||||
widget.contact,
|
||||
selection,
|
||||
success,
|
||||
null,
|
||||
);
|
||||
final repeater = _resolveRepeater(connector);
|
||||
connector.recordRepeaterPathResult(repeater, selection, success, null);
|
||||
_pendingStatusSelection = null;
|
||||
}
|
||||
|
||||
@@ -219,7 +196,8 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
final settings = context.watch<AppSettingsService>().settings;
|
||||
final isImperialUnits = settings.unitSystem == UnitSystem.imperial;
|
||||
final isFloodMode = widget.contact.pathOverride == -1;
|
||||
final repeater = _resolveRepeater(connector);
|
||||
final isFloodMode = repeater.pathOverride == -1;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -232,7 +210,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
widget.contact.name,
|
||||
repeater.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.normal,
|
||||
@@ -247,9 +225,9 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
tooltip: l10n.repeater_routingMode,
|
||||
onSelected: (mode) async {
|
||||
if (mode == 'flood') {
|
||||
await connector.setPathOverride(widget.contact, pathLen: -1);
|
||||
await connector.setPathOverride(repeater, pathLen: -1);
|
||||
} else {
|
||||
await connector.setPathOverride(widget.contact, pathLen: null);
|
||||
await connector.setPathOverride(repeater, pathLen: null);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
@@ -305,7 +283,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
icon: const Icon(Icons.timeline),
|
||||
tooltip: l10n.repeater_pathManagement,
|
||||
onPressed: () =>
|
||||
PathManagementDialog.show(context, contact: widget.contact),
|
||||
PathManagementDialog.show(context, contact: repeater),
|
||||
),
|
||||
IconButton(
|
||||
icon: _isLoading
|
||||
@@ -459,7 +437,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
final l10n = context.l10n;
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
final batteryMv =
|
||||
connector.getRepeaterBatteryMillivolts(widget.contact.publicKeyHex) ??
|
||||
connector.getRepeaterBatteryMillivolts(widget.repeater.publicKeyHex) ??
|
||||
(telemetryVolts == null ? null : (telemetryVolts * 1000).round());
|
||||
if (batteryMv == null) return l10n.common_notAvailable;
|
||||
final chemistry = _batteryChemistry();
|
||||
@@ -471,7 +449,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
String _batteryChemistry() {
|
||||
final settingsService = context.read<AppSettingsService>();
|
||||
return settingsService.batteryChemistryForRepeater(
|
||||
widget.contact.publicKeyHex,
|
||||
widget.repeater.publicKeyHex,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -51,14 +51,8 @@ class AppDebugLogService extends ChangeNotifier {
|
||||
String message, {
|
||||
String tag = 'App',
|
||||
AppDebugLogLevel level = AppDebugLogLevel.info,
|
||||
bool noNotify = false,
|
||||
}) {
|
||||
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(
|
||||
@@ -73,24 +67,22 @@ class AppDebugLogService extends ChangeNotifier {
|
||||
_entries.removeRange(0, _entries.length - maxEntries);
|
||||
}
|
||||
|
||||
if (!noNotify) {
|
||||
notifyListeners();
|
||||
}
|
||||
notifyListeners();
|
||||
|
||||
// Also print to console for development
|
||||
debugPrint('[$tag] $message');
|
||||
}
|
||||
|
||||
void info(String message, {String tag = 'App', bool noNotify = false}) {
|
||||
log(message, tag: tag, level: AppDebugLogLevel.info, noNotify: noNotify);
|
||||
void info(String message, {String tag = 'App'}) {
|
||||
log(message, tag: tag, level: AppDebugLogLevel.info);
|
||||
}
|
||||
|
||||
void warn(String message, {String tag = 'App', bool noNotify = false}) {
|
||||
log(message, tag: tag, level: AppDebugLogLevel.warning, noNotify: noNotify);
|
||||
void warn(String message, {String tag = 'App'}) {
|
||||
log(message, tag: tag, level: AppDebugLogLevel.warning);
|
||||
}
|
||||
|
||||
void error(String message, {String tag = 'App', bool noNotify = false}) {
|
||||
log(message, tag: tag, level: AppDebugLogLevel.error, noNotify: noNotify);
|
||||
void error(String message, {String tag = 'App'}) {
|
||||
log(message, tag: tag, level: AppDebugLogLevel.error);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
|
||||
@@ -80,10 +80,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));
|
||||
}
|
||||
@@ -134,10 +130,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,
|
||||
@@ -182,12 +174,4 @@ class AppSettingsService extends ChangeNotifier {
|
||||
..remove(channelName);
|
||||
await updateSettings(_settings.copyWith(mutedChannels: updated));
|
||||
}
|
||||
|
||||
Future<void> setTcpServerAddress(String value) async {
|
||||
await updateSettings(_settings.copyWith(tcpServerAddress: value));
|
||||
}
|
||||
|
||||
Future<void> setTcpServerPort(int value) async {
|
||||
await updateSettings(_settings.copyWith(tcpServerPort: value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import '../utils/platform_info.dart';
|
||||
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',
|
||||
@@ -28,7 +29,7 @@ class BackgroundService {
|
||||
}
|
||||
|
||||
Future<void> start() async {
|
||||
if (!PlatformInfo.isAndroid) return;
|
||||
if (!Platform.isAndroid) return;
|
||||
if (!_initialized) {
|
||||
await initialize();
|
||||
}
|
||||
@@ -42,7 +43,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();
|
||||
|
||||
@@ -172,6 +172,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 +215,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:
|
||||
|
||||
@@ -65,7 +65,7 @@ class ChatTextScaleService extends ChangeNotifier {
|
||||
|
||||
void _commitScale() {
|
||||
_saveTimer?.cancel();
|
||||
unawaited(PrefsManager.instance.setDouble(_prefKey, _scale));
|
||||
PrefsManager.instance.setDouble(_prefKey, _scale);
|
||||
}
|
||||
|
||||
double _clamp(double value) => value.clamp(_minScale, _maxScale).toDouble();
|
||||
|
||||
@@ -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,15 +231,6 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +281,6 @@ 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) {
|
||||
_debugLogService?.warn(
|
||||
'RESP_CODE_SENT: ACK hash $ackHashHex not found in hash table, falling back to queue',
|
||||
@@ -374,16 +290,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,10 +304,21 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,33 +357,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(
|
||||
@@ -547,7 +463,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 +499,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 +594,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 +616,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 +646,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 +783,6 @@ class MessageRetryService extends ChangeNotifier {
|
||||
_ackHistory.clear();
|
||||
_ackHashToMessageId.clear();
|
||||
_pendingMessageQueuePerContact.clear();
|
||||
_sendQueue.clear();
|
||||
_activeMessages.clear();
|
||||
_resolvedMessages.clear();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
@@ -65,27 +63,14 @@ class NotificationService {
|
||||
appUserModelId: 'org.meshcore.open.app',
|
||||
guid: 'e7ea8f85-72f5-4f36-91f6-038f740ccf86',
|
||||
);
|
||||
const linuxSettings = LinuxInitializationSettings(
|
||||
defaultActionName: 'Open notification',
|
||||
);
|
||||
|
||||
const initSettings = InitializationSettings(
|
||||
android: androidSettings,
|
||||
iOS: iosSettings,
|
||||
macOS: macSettings,
|
||||
windows: windowsSettings,
|
||||
linux: linuxSettings,
|
||||
);
|
||||
|
||||
// On Linux, the notifications plugin opens a D-Bus session bus
|
||||
// connection whose async subscription can throw an unhandled
|
||||
// SocketException when the bus socket is missing (e.g. running as
|
||||
// root or inside a container without a session bus).
|
||||
if (PlatformInfo.isLinux && !_isDbusSessionAvailable()) {
|
||||
debugPrint('Skipping notification init: D-Bus session bus unavailable');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await _notifications.initialize(
|
||||
settings: initSettings,
|
||||
@@ -97,15 +82,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();
|
||||
@@ -232,9 +208,7 @@ class NotificationService {
|
||||
|
||||
try {
|
||||
await _notifications.show(
|
||||
id: contactId != null
|
||||
? 'advert:$contactId'.hashCode
|
||||
: DateTime.now().millisecondsSinceEpoch,
|
||||
id: contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
|
||||
title: _l10n.notification_newTypeDiscovered(contactType),
|
||||
body: contactName,
|
||||
notificationDetails: notificationDetails,
|
||||
@@ -333,61 +307,6 @@ class NotificationService {
|
||||
await _notifications.cancel(id: id);
|
||||
}
|
||||
|
||||
/// Cancel the notification for a specific contact and update the app badge.
|
||||
Future<void> clearContactNotification(
|
||||
String contactId,
|
||||
int totalUnreadCount,
|
||||
) async {
|
||||
if (!await _ensureInitialized()) return;
|
||||
await _notifications.cancel(id: contactId.hashCode);
|
||||
await _updateBadge(totalUnreadCount);
|
||||
}
|
||||
|
||||
/// Cancel the notification for a specific channel and update the app badge.
|
||||
Future<void> clearChannelNotification(
|
||||
int channelIndex,
|
||||
int totalUnreadCount,
|
||||
) async {
|
||||
if (!await _ensureInitialized()) return;
|
||||
await _notifications.cancel(id: channelIndex.hashCode);
|
||||
await _updateBadge(totalUnreadCount);
|
||||
}
|
||||
|
||||
/// Cancel advert notifications for the given contact public key hexes.
|
||||
Future<void> clearAdvertNotifications(List<String> contactIds) async {
|
||||
if (!await _ensureInitialized()) return;
|
||||
for (final id in contactIds) {
|
||||
await _notifications.cancel(id: 'advert:$id'.hashCode);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _updateBadge(int count) async {
|
||||
if (PlatformInfo.isIOS || PlatformInfo.isMacOS) {
|
||||
// On Apple platforms, set the badge number directly via a silent update.
|
||||
final darwinDetails = DarwinNotificationDetails(
|
||||
presentAlert: false,
|
||||
presentSound: false,
|
||||
presentBadge: true,
|
||||
badgeNumber: count,
|
||||
);
|
||||
final details = NotificationDetails(
|
||||
iOS: darwinDetails,
|
||||
macOS: darwinDetails,
|
||||
);
|
||||
// Use a fixed ID so each update replaces the previous one.
|
||||
await _notifications.show(
|
||||
id: 'badge_update'.hashCode,
|
||||
title: null,
|
||||
body: null,
|
||||
notificationDetails: details,
|
||||
);
|
||||
// Immediately cancel the silent notification so it doesn't appear in tray.
|
||||
await _notifications.cancel(id: 'badge_update'.hashCode);
|
||||
}
|
||||
// On Android, badge count is derived from active notifications,
|
||||
// so cancelling the specific notification above is sufficient.
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// Public notification methods (rate limiting is enforced automatically)
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../storage/prefs_manager.dart';
|
||||
import '../utils/contact_search.dart';
|
||||
|
||||
const String contactsAllGroupsValue = '__all__';
|
||||
|
||||
enum ChannelSortOption { manual, name, latestMessages, unread }
|
||||
|
||||
class UiViewStateService extends ChangeNotifier {
|
||||
static const _keyContactsSelectedGroupName = 'ui_contacts_selected_group';
|
||||
static const _keyContactsSortOption = 'ui_contacts_sort_option';
|
||||
static const _keyContactsShowUnreadOnly = 'ui_contacts_show_unread_only';
|
||||
static const _keyContactsTypeFilter = 'ui_contacts_type_filter';
|
||||
static const _keyChannelsSortOption = 'ui_channels_sort_option';
|
||||
static const _keyChannelsSortIndexLegacy = 'ui_channels_sort_index';
|
||||
|
||||
String _contactsSelectedGroupName = contactsAllGroupsValue;
|
||||
String _contactsSearchText = '';
|
||||
bool _contactsSearchExpanded = false;
|
||||
ContactSortOption _contactsSortOption = ContactSortOption.lastSeen;
|
||||
bool _contactsShowUnreadOnly = false;
|
||||
ContactTypeFilter _contactsTypeFilter = ContactTypeFilter.all;
|
||||
|
||||
String _channelsSearchText = '';
|
||||
ChannelSortOption _channelsSortOption = ChannelSortOption.manual;
|
||||
|
||||
String get contactsSelectedGroupName => _contactsSelectedGroupName;
|
||||
String get contactsSearchText => _contactsSearchText;
|
||||
bool get contactsSearchExpanded => _contactsSearchExpanded;
|
||||
ContactSortOption get contactsSortOption => _contactsSortOption;
|
||||
bool get contactsShowUnreadOnly => _contactsShowUnreadOnly;
|
||||
ContactTypeFilter get contactsTypeFilter => _contactsTypeFilter;
|
||||
String get channelsSearchText => _channelsSearchText;
|
||||
ChannelSortOption get channelsSortOption => _channelsSortOption;
|
||||
|
||||
Future<void> initialize() async {
|
||||
final prefs = PrefsManager.instance;
|
||||
|
||||
final selectedGroupName = prefs.getString(_keyContactsSelectedGroupName);
|
||||
if (selectedGroupName != null && selectedGroupName.isNotEmpty) {
|
||||
_contactsSelectedGroupName = selectedGroupName;
|
||||
}
|
||||
|
||||
final sortStr = prefs.getString(_keyContactsSortOption);
|
||||
if (sortStr != null) {
|
||||
_contactsSortOption = ContactSortOption.values.firstWhere(
|
||||
(e) => e.name == sortStr,
|
||||
orElse: () => ContactSortOption.lastSeen,
|
||||
);
|
||||
}
|
||||
|
||||
_contactsShowUnreadOnly =
|
||||
prefs.getBool(_keyContactsShowUnreadOnly) ?? false;
|
||||
|
||||
final typeStr = prefs.getString(_keyContactsTypeFilter);
|
||||
if (typeStr != null) {
|
||||
_contactsTypeFilter = ContactTypeFilter.values.firstWhere(
|
||||
(e) => e.name == typeStr,
|
||||
orElse: () => ContactTypeFilter.all,
|
||||
);
|
||||
}
|
||||
|
||||
final channelSortStr = prefs.getString(_keyChannelsSortOption);
|
||||
if (channelSortStr != null) {
|
||||
_channelsSortOption = ChannelSortOption.values.firstWhere(
|
||||
(e) => e.name == channelSortStr,
|
||||
orElse: () => ChannelSortOption.manual,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Backward compatibility for old persisted index format.
|
||||
switch (prefs.getInt(_keyChannelsSortIndexLegacy) ?? 0) {
|
||||
case 0:
|
||||
_channelsSortOption = ChannelSortOption.manual;
|
||||
break;
|
||||
case 1:
|
||||
_channelsSortOption = ChannelSortOption.name;
|
||||
break;
|
||||
case 2:
|
||||
_channelsSortOption = ChannelSortOption.latestMessages;
|
||||
break;
|
||||
case 3:
|
||||
_channelsSortOption = ChannelSortOption.unread;
|
||||
break;
|
||||
default:
|
||||
_channelsSortOption = ChannelSortOption.manual;
|
||||
}
|
||||
}
|
||||
|
||||
void setContactsSelectedGroupName(String value) {
|
||||
if (_contactsSelectedGroupName == value) return;
|
||||
_contactsSelectedGroupName = value;
|
||||
notifyListeners();
|
||||
unawaited(
|
||||
PrefsManager.instance.setString(_keyContactsSelectedGroupName, value),
|
||||
);
|
||||
}
|
||||
|
||||
void setContactsSearchText(String value) {
|
||||
if (_contactsSearchText == value) return;
|
||||
_contactsSearchText = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setContactsSearchExpanded(bool value) {
|
||||
if (_contactsSearchExpanded == value) return;
|
||||
_contactsSearchExpanded = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setContactsSortOption(ContactSortOption value) {
|
||||
if (_contactsSortOption == value) return;
|
||||
_contactsSortOption = value;
|
||||
notifyListeners();
|
||||
unawaited(
|
||||
PrefsManager.instance.setString(_keyContactsSortOption, value.name),
|
||||
);
|
||||
}
|
||||
|
||||
void setContactsShowUnreadOnly(bool value) {
|
||||
if (_contactsShowUnreadOnly == value) return;
|
||||
_contactsShowUnreadOnly = value;
|
||||
notifyListeners();
|
||||
unawaited(PrefsManager.instance.setBool(_keyContactsShowUnreadOnly, value));
|
||||
}
|
||||
|
||||
void setContactsTypeFilter(ContactTypeFilter value) {
|
||||
if (_contactsTypeFilter == value) return;
|
||||
_contactsTypeFilter = value;
|
||||
notifyListeners();
|
||||
unawaited(
|
||||
PrefsManager.instance.setString(_keyContactsTypeFilter, value.name),
|
||||
);
|
||||
}
|
||||
|
||||
void setChannelsSearchText(String value) {
|
||||
if (_channelsSearchText == value) return;
|
||||
_channelsSearchText = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setChannelsSortOption(ChannelSortOption value) {
|
||||
if (_channelsSortOption == value) return;
|
||||
_channelsSortOption = value;
|
||||
notifyListeners();
|
||||
unawaited(
|
||||
PrefsManager.instance.setString(_keyChannelsSortOption, value.name),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export 'usb_serial_service_native.dart'
|
||||
if (dart.library.js_interop) 'usb_serial_service_web.dart';
|
||||
@@ -1,484 +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);
|
||||
// Toggle DTR low→high so the device sees a fresh connection even
|
||||
// if the previous disconnect didn't cleanly signal DTR drop.
|
||||
serial.setDTR(false);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
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> writeRaw(Uint8List data) async {
|
||||
if (!isConnected) {
|
||||
throw StateError('USB serial port is not open');
|
||||
}
|
||||
if (_useAndroidUsbHost) {
|
||||
try {
|
||||
await _androidMethodChannel.invokeMethod<void>('write', {'data': data});
|
||||
} on PlatformException catch (error) {
|
||||
throw StateError(error.message ?? error.code);
|
||||
}
|
||||
} else {
|
||||
_serial!.write(data);
|
||||
}
|
||||
}
|
||||
|
||||
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?.setDTR(false);
|
||||
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?.setDTR(false);
|
||||
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 }
|
||||
@@ -1,591 +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> writeRaw(Uint8List data) async {
|
||||
if (!isConnected || _writer == null) {
|
||||
throw StateError('USB serial port is not open');
|
||||
}
|
||||
final promise = _writer!.callMethod<JSPromise<JSAny?>>(
|
||||
'write'.toJS,
|
||||
data.toJS,
|
||||
);
|
||||
await promise.toDart;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,46 +2,18 @@ import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../models/channel.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import 'prefs_manager.dart';
|
||||
|
||||
class ChannelStore {
|
||||
static const String _keyPrefix = 'channels';
|
||||
String publicKeyHex = '';
|
||||
set setPublicKeyHex(String value) =>
|
||||
publicKeyHex = value.length >= 10 ? value.substring(0, 10) : '';
|
||||
|
||||
String get keyFor => '$_keyPrefix$publicKeyHex';
|
||||
static const String _key = 'channels';
|
||||
|
||||
Future<List<Channel>> loadChannels() async {
|
||||
if (publicKeyHex.isEmpty) {
|
||||
appLogger.warn('Public key hex is not set. Cannot load channels.');
|
||||
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 messages from legacy key $_keyPrefix to scoped key $keyFor',
|
||||
);
|
||||
await prefs.setString(keyFor, legacyJsonString);
|
||||
jsonString = legacyJsonString;
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
jsonString = prefs.getString(keyFor);
|
||||
}
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
final jsonStr = prefs.getString(_key);
|
||||
if (jsonStr == null) return [];
|
||||
|
||||
try {
|
||||
final jsonList = jsonDecode(jsonString) as List<dynamic>;
|
||||
final jsonList = jsonDecode(jsonStr) as List<dynamic>;
|
||||
return jsonList
|
||||
.map((entry) => _fromJson(entry as Map<String, dynamic>))
|
||||
.toList();
|
||||
@@ -51,13 +23,9 @@ class ChannelStore {
|
||||
}
|
||||
|
||||
Future<void> saveChannels(List<Channel> channels) async {
|
||||
if (publicKeyHex.isEmpty) {
|
||||
appLogger.warn('Public key hex is not set. Cannot save channels.');
|
||||
return;
|
||||
}
|
||||
final prefs = PrefsManager.instance;
|
||||
final jsonList = channels.map(_toJson).toList();
|
||||
await prefs.setString(keyFor, jsonEncode(jsonList));
|
||||
await prefs.setString(_key, jsonEncode(jsonList));
|
||||
}
|
||||
|
||||
Map<String, dynamic> _toJson(Channel channel) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../models/community.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import 'prefs_manager.dart';
|
||||
|
||||
/// Persists communities to local storage using SharedPreferences.
|
||||
@@ -10,37 +9,12 @@ import 'prefs_manager.dart';
|
||||
/// Each community contains its secret K, so this data should
|
||||
/// be considered sensitive (though device encryption handles security).
|
||||
class CommunityStore {
|
||||
static const String _keyPrefix = 'communities_v1';
|
||||
|
||||
String publicKeyHex = '';
|
||||
set setPublicKeyHex(String value) =>
|
||||
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
|
||||
|
||||
String get keyFor => '$_keyPrefix$publicKeyHex';
|
||||
static const String _communitiesKey = 'communities_v1';
|
||||
|
||||
/// Load all communities from storage
|
||||
Future<List<Community>> loadCommunities() async {
|
||||
if (publicKeyHex.isEmpty) {
|
||||
appLogger.warn('Public key hex is not set. Cannot load communities.');
|
||||
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 communities from legacy key $_keyPrefix to scoped key $keyFor',
|
||||
);
|
||||
await prefs.setString(keyFor, legacyJsonString);
|
||||
jsonString = legacyJsonString;
|
||||
}
|
||||
}
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
jsonString = prefs.getString(keyFor);
|
||||
}
|
||||
final jsonString = prefs.getString(_communitiesKey);
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
@@ -58,13 +32,9 @@ class CommunityStore {
|
||||
|
||||
/// Save all communities to storage
|
||||
Future<void> saveCommunities(List<Community> communities) async {
|
||||
if (publicKeyHex.isEmpty) {
|
||||
appLogger.warn('Public key hex is not set. Cannot save communities.');
|
||||
return;
|
||||
}
|
||||
final prefs = PrefsManager.instance;
|
||||
final jsonList = communities.map((c) => c.toJson()).toList();
|
||||
await prefs.setString(keyFor, jsonEncode(jsonList));
|
||||
await prefs.setString(_communitiesKey, jsonEncode(jsonList));
|
||||
}
|
||||
|
||||
/// Add a new community
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../models/contact.dart';
|
||||
import 'prefs_manager.dart';
|
||||
|
||||
class ContactDiscoveryStore {
|
||||
static const String _keyPrefix = 'discovered_contacts';
|
||||
|
||||
Future<List<Contact>> loadContacts() async {
|
||||
final prefs = PrefsManager.instance;
|
||||
final jsonStr = prefs.getString(_keyPrefix);
|
||||
if (jsonStr == null) return [];
|
||||
|
||||
try {
|
||||
final jsonList = jsonDecode(jsonStr) as List<dynamic>;
|
||||
return jsonList
|
||||
.map((entry) => _fromJson(entry as Map<String, dynamic>))
|
||||
.toList();
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveContacts(List<Contact> contacts) async {
|
||||
final prefs = PrefsManager.instance;
|
||||
final jsonList = contacts.map(_toJson).toList();
|
||||
await prefs.setString(_keyPrefix, jsonEncode(jsonList));
|
||||
}
|
||||
|
||||
Map<String, dynamic> _toJson(Contact contact) {
|
||||
return {
|
||||
'publicKey': base64Encode(contact.publicKey),
|
||||
'name': contact.name,
|
||||
'type': contact.type,
|
||||
'flags': contact.flags,
|
||||
'pathLength': contact.pathLength,
|
||||
'path': base64Encode(contact.path),
|
||||
'pathOverride': contact.pathOverride,
|
||||
'pathOverrideBytes': contact.pathOverrideBytes != null
|
||||
? base64Encode(contact.pathOverrideBytes!)
|
||||
: null,
|
||||
'latitude': contact.latitude,
|
||||
'longitude': contact.longitude,
|
||||
'lastSeen': contact.lastSeen.millisecondsSinceEpoch,
|
||||
'lastMessageAt': contact.lastMessageAt.millisecondsSinceEpoch,
|
||||
'rawPacket': contact.rawPacket != null
|
||||
? base64Encode(contact.rawPacket!)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
Contact _fromJson(Map<String, dynamic> json) {
|
||||
final lastSeenMs = json['lastSeen'] as int? ?? 0;
|
||||
final lastMessageMs = json['lastMessageAt'] as int?;
|
||||
return Contact(
|
||||
publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)),
|
||||
name: json['name'] as String? ?? 'Unknown',
|
||||
type: json['type'] as int? ?? 0,
|
||||
flags: json['flags'] as int? ?? 0,
|
||||
pathLength: json['pathLength'] as int? ?? -1,
|
||||
path: json['path'] != null
|
||||
? Uint8List.fromList(base64Decode(json['path'] as String))
|
||||
: Uint8List(0),
|
||||
pathOverride: json['pathOverride'] as int?,
|
||||
pathOverrideBytes: json['pathOverrideBytes'] != null
|
||||
? Uint8List.fromList(
|
||||
base64Decode(json['pathOverrideBytes'] as String),
|
||||
)
|
||||
: null,
|
||||
latitude: (json['latitude'] as num?)?.toDouble(),
|
||||
longitude: (json['longitude'] as num?)?.toDouble(),
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastSeenMs),
|
||||
lastMessageAt: DateTime.fromMillisecondsSinceEpoch(
|
||||
lastMessageMs ?? lastSeenMs,
|
||||
),
|
||||
isActive: false,
|
||||
rawPacket: json['rawPacket'] != null
|
||||
? Uint8List.fromList(base64Decode(json['rawPacket'] as String))
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,17 @@
|
||||
import 'dart:convert';
|
||||
import '../models/contact_group.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import 'prefs_manager.dart';
|
||||
|
||||
class ContactGroupStore {
|
||||
static const String _keyPrefix = 'contact_groups';
|
||||
|
||||
String publicKeyHex = '';
|
||||
set setPublicKeyHex(String value) =>
|
||||
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
|
||||
|
||||
String get keyFor => '$_keyPrefix$publicKeyHex';
|
||||
static const String _key = 'contact_groups';
|
||||
|
||||
Future<List<ContactGroup>> loadGroups() async {
|
||||
if (publicKeyHex.isEmpty) {
|
||||
appLogger.warn('Public key hex is not set. Cannot load contact groups.');
|
||||
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 messages from legacy key $_keyPrefix to scoped key $keyFor',
|
||||
);
|
||||
await prefs.setString(keyFor, legacyJsonString);
|
||||
jsonString = legacyJsonString;
|
||||
}
|
||||
}
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
jsonString = prefs.getString(keyFor);
|
||||
}
|
||||
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
|
||||
.whereType<Map<String, dynamic>>()
|
||||
@@ -53,12 +25,8 @@ class ContactGroupStore {
|
||||
}
|
||||
|
||||
Future<void> saveGroups(List<ContactGroup> groups) async {
|
||||
if (publicKeyHex.isEmpty) {
|
||||
appLogger.warn('Public key hex is not set. Cannot save contact groups.');
|
||||
return;
|
||||
}
|
||||
final prefs = PrefsManager.instance;
|
||||
final encoded = jsonEncode(groups.map((group) => group.toJson()).toList());
|
||||
await prefs.setString(keyFor, encoded);
|
||||
await prefs.setString(_key, encoded);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,17 @@
|
||||
import '../utils/app_logger.dart';
|
||||
import 'prefs_manager.dart';
|
||||
|
||||
class ContactSettingsStore {
|
||||
static const String _keyPrefix = 'contact_smaz_';
|
||||
|
||||
String publicKeyHex = '';
|
||||
set setPublicKeyHex(String value) =>
|
||||
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
|
||||
|
||||
String get keyFor => '$_keyPrefix$publicKeyHex';
|
||||
static const String _smazKeyPrefix = 'contact_smaz_';
|
||||
|
||||
Future<bool> loadSmazEnabled(String contactKeyHex) async {
|
||||
if (publicKeyHex.isEmpty) {
|
||||
appLogger.warn(
|
||||
'Public key hex is not set. Cannot load contact settings.',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
final prefs = PrefsManager.instance;
|
||||
final key = '$keyFor$contactKeyHex';
|
||||
final oldKey = '$_keyPrefix$contactKeyHex';
|
||||
bool? enabled = prefs.getBool(key);
|
||||
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 contact settings from legacy key $oldKey to scoped key $key',
|
||||
);
|
||||
await prefs.setBool(key, enabled);
|
||||
}
|
||||
}
|
||||
final key = '$_smazKeyPrefix$contactKeyHex';
|
||||
return prefs.getBool(key) ?? false;
|
||||
}
|
||||
|
||||
Future<void> saveSmazEnabled(String contactKeyHex, bool enabled) async {
|
||||
if (publicKeyHex.isEmpty) {
|
||||
appLogger.warn(
|
||||
'Public key hex is not set. Cannot save contact settings.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
final prefs = PrefsManager.instance;
|
||||
final key = '$keyFor$contactKeyHex';
|
||||
final key = '$_smazKeyPrefix$contactKeyHex';
|
||||
await prefs.setBool(key, enabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,46 +2,18 @@ import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../models/contact.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import 'prefs_manager.dart';
|
||||
|
||||
class ContactStore {
|
||||
static const String _keyPrefix = 'contacts';
|
||||
|
||||
String publicKeyHex = '';
|
||||
set setPublicKeyHex(String value) =>
|
||||
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
|
||||
|
||||
String get keyFor => '$_keyPrefix$publicKeyHex';
|
||||
static const String _key = 'contacts';
|
||||
|
||||
Future<List<Contact>> loadContacts() async {
|
||||
if (publicKeyHex.isEmpty) {
|
||||
appLogger.warn('Public key hex is not set. Cannot load contacts.');
|
||||
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 contacts from legacy key $_keyPrefix to scoped key $keyFor',
|
||||
);
|
||||
await prefs.setString(keyFor, legacyJsonString);
|
||||
jsonString = legacyJsonString;
|
||||
}
|
||||
}
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
jsonString = prefs.getString(keyFor);
|
||||
}
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
final jsonStr = prefs.getString(_key);
|
||||
if (jsonStr == null) return [];
|
||||
|
||||
try {
|
||||
final jsonList = jsonDecode(jsonString) as List<dynamic>;
|
||||
final jsonList = jsonDecode(jsonStr) as List<dynamic>;
|
||||
return jsonList
|
||||
.map((entry) => _fromJson(entry as Map<String, dynamic>))
|
||||
.toList();
|
||||
@@ -51,13 +23,9 @@ class ContactStore {
|
||||
}
|
||||
|
||||
Future<void> saveContacts(List<Contact> contacts) async {
|
||||
if (publicKeyHex.isEmpty) {
|
||||
appLogger.warn('Public key hex is not set. Cannot save contacts.');
|
||||
return;
|
||||
}
|
||||
final prefs = PrefsManager.instance;
|
||||
final jsonList = contacts.map(_toJson).toList();
|
||||
await prefs.setString(keyFor, jsonEncode(jsonList));
|
||||
await prefs.setString(_key, jsonEncode(jsonList));
|
||||
}
|
||||
|
||||
Map<String, dynamic> _toJson(Contact contact) {
|
||||
@@ -76,10 +44,6 @@ class ContactStore {
|
||||
'longitude': contact.longitude,
|
||||
'lastSeen': contact.lastSeen.millisecondsSinceEpoch,
|
||||
'lastMessageAt': contact.lastMessageAt.millisecondsSinceEpoch,
|
||||
'isActive': contact.isActive,
|
||||
'rawPacket': contact.rawPacket != null
|
||||
? base64Encode(contact.rawPacket!)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -107,10 +71,6 @@ class ContactStore {
|
||||
lastMessageAt: DateTime.fromMillisecondsSinceEpoch(
|
||||
lastMessageMs ?? lastSeenMs,
|
||||
),
|
||||
isActive: json['isActive'] as bool? ?? true,
|
||||
rawPacket: json['rawPacket'] != null
|
||||
? Uint8List.fromList(base64Decode(json['rawPacket'] as String))
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,59 +2,26 @@ import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import '../models/message.dart';
|
||||
import '../helpers/smaz.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import 'prefs_manager.dart';
|
||||
|
||||
class MessageStore {
|
||||
static const String _keyPrefix = 'messages_';
|
||||
|
||||
String publicKeyHex = '';
|
||||
set setPublicKeyHex(String value) =>
|
||||
publicKeyHex = value.length > 10 ? value.substring(0, 10) : '';
|
||||
|
||||
String get keyFor => '$_keyPrefix$publicKeyHex';
|
||||
|
||||
Future<void> saveMessages(
|
||||
String contactKeyHex,
|
||||
List<Message> messages,
|
||||
) async {
|
||||
if (publicKeyHex.isEmpty) {
|
||||
appLogger.warn('Public key hex is not set. Cannot save messages.');
|
||||
return;
|
||||
}
|
||||
final prefs = PrefsManager.instance;
|
||||
final key = '$keyFor$contactKeyHex';
|
||||
final key = '$_keyPrefix$contactKeyHex';
|
||||
final jsonList = messages.map(_messageToJson).toList();
|
||||
await prefs.setString(key, jsonEncode(jsonList));
|
||||
}
|
||||
|
||||
Future<List<Message>> loadMessages(String contactKeyHex) async {
|
||||
if (publicKeyHex.isEmpty) {
|
||||
appLogger.warn('Public key hex is not set. Cannot load messages.');
|
||||
return [];
|
||||
}
|
||||
final prefs = PrefsManager.instance;
|
||||
final key = '$keyFor$contactKeyHex';
|
||||
final oldKey = '$_keyPrefix$contactKeyHex';
|
||||
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 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 [];
|
||||
}
|
||||
final key = '$_keyPrefix$contactKeyHex';
|
||||
final jsonString = prefs.getString(key);
|
||||
if (jsonString == null) return [];
|
||||
|
||||
try {
|
||||
final jsonList = jsonDecode(jsonString) as List<dynamic>;
|
||||
@@ -65,12 +32,8 @@ class MessageStore {
|
||||
}
|
||||
|
||||
Future<void> clearMessages(String contactKeyHex) async {
|
||||
if (publicKeyHex.isEmpty) {
|
||||
appLogger.warn('Public key hex is not set. Cannot clear messages.');
|
||||
return;
|
||||
}
|
||||
final prefs = PrefsManager.instance;
|
||||
final key = '$keyFor$contactKeyHex';
|
||||
final key = '$_keyPrefix$contactKeyHex';
|
||||
await prefs.remove(key);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import '../utils/app_logger.dart';
|
||||
import 'prefs_manager.dart';
|
||||
|
||||
/// Storage for unread message tracking with debounced writes to reduce I/O.
|
||||
class UnreadStore {
|
||||
static const String _keyPrefix = 'contact_unread_count';
|
||||
|
||||
String publicKeyHex = '';
|
||||
set setPublicKeyHex(String value) =>
|
||||
publicKeyHex = value.length >= 10 ? value.substring(0, 10) : '';
|
||||
|
||||
String get keyFor => '$_keyPrefix$publicKeyHex';
|
||||
static const String _contactUnreadCountKey = 'contact_unread_count';
|
||||
|
||||
// Debounce timers to batch rapid writes
|
||||
Timer? _contactUnreadSaveTimer;
|
||||
@@ -27,33 +20,12 @@ class UnreadStore {
|
||||
}
|
||||
|
||||
Future<Map<String, int>> loadContactUnreadCount() async {
|
||||
if (publicKeyHex.isEmpty) {
|
||||
appLogger.warn('Public key hex is not set. Cannot load unread counts.');
|
||||
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 messages from legacy key $_keyPrefix to scoped key $keyFor',
|
||||
);
|
||||
await prefs.setString(keyFor, legacyJsonString);
|
||||
jsonString = legacyJsonString;
|
||||
}
|
||||
}
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
jsonString = prefs.getString(keyFor);
|
||||
}
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return {};
|
||||
}
|
||||
final jsonStr = prefs.getString(_contactUnreadCountKey);
|
||||
if (jsonStr == null) return {};
|
||||
|
||||
try {
|
||||
final json = jsonDecode(jsonString) as Map<String, dynamic>;
|
||||
final json = jsonDecode(jsonStr) as Map<String, dynamic>;
|
||||
return json.map((key, value) => MapEntry(key, value as int));
|
||||
} catch (_) {
|
||||
return {};
|
||||
@@ -61,10 +33,6 @@ class UnreadStore {
|
||||
}
|
||||
|
||||
void saveContactUnreadCount(Map<String, int> counts) {
|
||||
if (publicKeyHex.isEmpty) {
|
||||
appLogger.warn('Public key hex is not set. Cannot save unread counts.');
|
||||
return;
|
||||
}
|
||||
_pendingContactUnreadCount = counts;
|
||||
|
||||
_contactUnreadSaveTimer?.cancel();
|
||||
@@ -81,7 +49,7 @@ class UnreadStore {
|
||||
|
||||
final prefs = PrefsManager.instance;
|
||||
final jsonStr = jsonEncode(_pendingContactUnreadCount);
|
||||
await prefs.setString(keyFor, jsonStr);
|
||||
await prefs.setString(_contactUnreadCountKey, jsonStr);
|
||||
_pendingContactUnreadCount = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,23 +23,23 @@ class AppLogger {
|
||||
bool get isEnabled => _enabled;
|
||||
|
||||
/// Log an info message
|
||||
void info(String message, {String tag = 'App', bool noNotify = false}) {
|
||||
void info(String message, {String tag = 'App'}) {
|
||||
if (_enabled && _service != null) {
|
||||
_service!.info(message, tag: tag, noNotify: noNotify);
|
||||
_service!.info(message, tag: tag);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log a warning message
|
||||
void warn(String message, {String tag = 'App', bool noNotify = false}) {
|
||||
void warn(String message, {String tag = 'App'}) {
|
||||
if (_enabled && _service != null) {
|
||||
_service!.warn(message, tag: tag, noNotify: noNotify);
|
||||
_service!.warn(message, tag: tag);
|
||||
}
|
||||
}
|
||||
|
||||
/// Log an error message
|
||||
void error(String message, {String tag = 'App', bool noNotify = false}) {
|
||||
void error(String message, {String tag = 'App'}) {
|
||||
if (_enabled && _service != null) {
|
||||
_service!.error(message, tag: tag, noNotify: noNotify);
|
||||
_service!.error(message, tag: tag);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,10 +48,9 @@ class AppLogger {
|
||||
String message, {
|
||||
String tag = 'App',
|
||||
AppDebugLogLevel level = AppDebugLogLevel.info,
|
||||
bool noNotify = false,
|
||||
}) {
|
||||
if (_enabled && _service != null) {
|
||||
_service!.log(message, tag: tag, level: level, noNotify: noNotify);
|
||||
_service!.log(message, tag: tag, level: level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export 'browser_detection_stub.dart'
|
||||
if (dart.library.js_interop) 'browser_detection_web.dart';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user