Refine USB transport flow

- replace Android USB dependency with app-owned USB host implementation\n- restore BLE-first scanner flow with USB secondary action\n- tighten Web Serial key handling and disconnect logging\n\nTODO (follow-up):\n- review non-English localization copy for tone and consistency\n- trim remaining unused/awkward localization strings introduced during USB UI changes
This commit is contained in:
just_stuff_tm
2026-03-02 22:48:19 -05:00
committed by just-stuff-tm
parent 74da9e82b5
commit 44c0670dae
45 changed files with 16316 additions and 15541 deletions
+1 -2
View File
@@ -16,7 +16,7 @@ if (keystorePropertiesFile.exists()) {
android { android {
namespace = "com.meshcore.meshcore_open" namespace = "com.meshcore.meshcore_open"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
ndkVersion = "29.0.14206865" ndkVersion = flutter.ndkVersion
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_17
@@ -84,5 +84,4 @@ flutter {
dependencies { dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
implementation("com.github.mik3y:usb-serial-for-android:3.9.0")
} }
@@ -1,408 +1,18 @@
package com.meshcore.meshcore_open 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.UsbDevice
import android.hardware.usb.UsbDeviceConnection
import android.hardware.usb.UsbManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import com.hoho.android.usbserial.driver.UsbSerialDriver
import com.hoho.android.usbserial.driver.UsbSerialPort
import com.hoho.android.usbserial.driver.UsbSerialProber
import com.hoho.android.usbserial.util.SerialInputOutputManager
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine 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 MainActivity : FlutterActivity() { class MainActivity : FlutterActivity() {
private val usbMethodChannelName = "meshcore_open/android_usb_serial" private val usbFunctions by lazy { MeshcoreUsbFunctions(this) }
private val usbEventChannelName = "meshcore_open/android_usb_serial_events"
private val usbPermissionAction = "com.meshcore.meshcore_open.USB_PERMISSION"
private lateinit var usbManager: UsbManager
private val mainHandler = Handler(Looper.getMainLooper())
private val usbIoExecutor: ExecutorService = Executors.newSingleThreadExecutor()
private var eventSink: EventChannel.EventSink? = null
private var usbConnection: UsbDeviceConnection? = null
private var usbPort: UsbSerialPort? = null
private var ioManager: SerialInputOutputManager? = null
private var connectedDeviceName: String? = null
private var pendingConnectResult: MethodChannel.Result? = null
private var pendingConnectPortName: String? = null
private var pendingConnectBaudRate: Int = 115200
private val permissionReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
UsbManager.ACTION_USB_DEVICE_DETACHED -> {
handleUsbDetached(intent)
return
}
usbPermissionAction -> {
}
else -> {
return
}
}
if (intent.action != usbPermissionAction) {
return
}
val result = pendingConnectResult
val portName = pendingConnectPortName
pendingConnectResult = null
pendingConnectPortName = null
if (result == null || portName == null) {
return
}
val granted =
intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
if (!granted) {
result.error("usb_permission_denied", "USB permission denied", null)
return
}
val device = findUsbDevice(portName)
if (device == null) {
result.error(
"usb_device_missing",
"USB device no longer available for $portName",
null,
)
return
}
openUsbDevice(device, pendingConnectBaudRate, result)
}
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
usbFunctions.configureFlutterEngine(flutterEngine)
usbManager = getSystemService(Context.USB_SERVICE) as UsbManager
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
}
},
)
} }
override fun onDestroy() { override fun onDestroy() {
closeUsbConnection() usbFunctions.dispose()
usbIoExecutor.shutdownNow()
try {
unregisterReceiver(permissionReceiver)
} catch (_: IllegalArgumentException) {
}
super.onDestroy() super.onDestroy()
} }
private fun registerUsbPermissionReceiver() {
val filter =
IntentFilter().apply {
addAction(usbPermissionAction)
addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(permissionReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else {
@Suppress("DEPRECATION")
registerReceiver(permissionReceiver, filter)
}
}
private fun listUsbPorts(): List<String> {
val drivers = UsbSerialProber.getDefaultProber().findAllDrivers(usbManager)
return drivers.map { driver ->
val device = driver.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", "Port name is required", null)
return
}
val device = findUsbDevice(portName)
if (device == null) {
result.error("usb_device_missing", "USB device not found for $portName", null)
return
}
if (usbManager.hasPermission(device)) {
openUsbDevice(device, baudRate, result)
return
}
if (pendingConnectResult != null) {
result.error("usb_busy", "Another USB permission request is already pending", null)
return
}
pendingConnectResult = result
pendingConnectPortName = portName
pendingConnectBaudRate = baudRate
val permissionIntent = PendingIntent.getBroadcast(
this,
0,
Intent(usbPermissionAction).setPackage(packageName),
pendingIntentFlags(),
)
usbManager.requestPermission(device, permissionIntent)
}
private fun handleUsbWrite(call: MethodCall, result: MethodChannel.Result) {
val data = call.argument<ByteArray>("data")
val port = usbPort
if (data == null) {
result.error("usb_invalid_data", "Data is required", null)
return
}
if (port == null) {
result.error("usb_not_connected", "USB serial port is not connected", null)
return
}
usbIoExecutor.execute {
try {
port.write(data, 1000)
mainHandler.post {
result.success(null)
}
} catch (error: Exception) {
mainHandler.post {
result.error("usb_write_failed", error.message, null)
}
}
}
}
private fun findUsbDevice(portName: String): UsbDevice? {
return usbManager.deviceList.values.firstOrNull { it.deviceName == portName }
}
private fun openUsbDevice(
device: UsbDevice,
baudRate: Int,
result: MethodChannel.Result,
) {
usbIoExecutor.execute {
try {
closeUsbConnection()
val driver = UsbSerialProber.getDefaultProber().probeDevice(device)
if (driver == null) {
mainHandler.post {
result.error(
"usb_driver_missing",
"No USB serial driver for ${device.deviceName}",
null,
)
}
return@execute
}
val connection = usbManager.openDevice(device)
if (connection == null) {
mainHandler.post {
result.error(
"usb_open_failed",
"UsbManager could not open ${device.deviceName}",
null,
)
}
return@execute
}
val port = firstPort(driver)
if (port == null) {
connection.close()
mainHandler.post {
result.error(
"usb_port_missing",
"No USB serial port exposed by ${device.deviceName}",
null,
)
}
return@execute
}
port.open(connection)
port.setParameters(
baudRate,
8,
UsbSerialPort.STOPBITS_1,
UsbSerialPort.PARITY_NONE,
)
port.rts = false
port.dtr = true
usbConnection = connection
usbPort = port
connectedDeviceName = device.deviceName
ioManager =
SerialInputOutputManager(
port,
object : SerialInputOutputManager.Listener {
override fun onNewData(data: ByteArray) {
mainHandler.post {
eventSink?.success(data)
}
}
override fun onRunError(e: Exception) {
mainHandler.post {
eventSink?.error(
"usb_io_error",
e.message ?: "USB serial I/O error",
null,
)
}
scheduleCloseUsbConnection()
}
},
).also { manager ->
manager.start()
}
mainHandler.post {
result.success(null)
}
} catch (error: Exception) {
closeUsbConnection()
mainHandler.post {
result.error("usb_connect_failed", error.message, null)
}
}
}
}
private fun firstPort(driver: UsbSerialDriver): UsbSerialPort? {
return driver.ports.firstOrNull()
}
private fun scheduleCloseUsbConnection(onComplete: (() -> Unit)? = null) {
usbIoExecutor.execute {
closeUsbConnection()
if (onComplete != null) {
mainHandler.post(onComplete)
}
}
}
@Synchronized
private fun closeUsbConnection() {
try {
ioManager?.stop()
} catch (_: Exception) {
}
ioManager = null
try {
usbPort?.close()
} catch (_: Exception) {
}
usbPort = null
try {
usbConnection?.close()
} catch (_: Exception) {
}
usbConnection = null
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
}
} }
@@ -0,0 +1,574 @@
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()
private var eventSink: EventChannel.EventSink? = null
private var usbConnection: UsbDeviceConnection? = null
private var usbInEndpoint: UsbEndpoint? = null
private var usbOutEndpoint: UsbEndpoint? = null
private var controlInterface: UsbInterface? = null
private var dataInterface: UsbInterface? = null
private var readThread: Thread? = null
@Volatile private var isReading = false
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 granted =
intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
if (!granted) {
result.error("usb_permission_denied", "USB permission denied", null)
return
}
val device = findUsbDevice(portName)
if (device == null) {
result.error(
"usb_device_missing",
"USB device no longer available for $portName",
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", "Port name is required", null)
return
}
val device = findUsbDevice(portName)
if (device == null) {
result.error("usb_device_missing", "USB device not found for $portName", null)
return
}
if (usbManager.hasPermission(device)) {
openUsbDevice(device, baudRate, result)
return
}
if (pendingConnectResult != null) {
result.error("usb_busy", "Another USB permission request is already pending", 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", "Data is required", null)
return
}
if (connection == null || endpoint == null) {
result.error("usb_not_connected", "USB serial port is not connected", 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? {
return usbManager.deviceList.values.firstOrNull { it.deviceName == portName }
}
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",
"No compatible USB serial interface for ${device.deviceName}",
null,
)
}
return@execute
}
val connection = usbManager.openDevice(device)
if (connection == null) {
mainHandler.post {
result.error(
"usb_open_failed",
"UsbManager could not open ${device.deviceName}",
null,
)
}
return@execute
}
if (!connection.claimInterface(config.dataInterface, true)) {
connection.close()
mainHandler.post {
result.error(
"usb_open_failed",
"Could not claim USB data interface for ${device.deviceName}",
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",
"Could not claim USB control interface for ${device.deviceName}",
null,
)
}
return@execute
}
configureDevice(connection, config, baudRate)
usbConnection = connection
usbInEndpoint = config.inEndpoint
usbOutEndpoint = config.outEndpoint
controlInterface = config.controlInterface
dataInterface = config.dataInterface
connectedDeviceName = device.deviceName
startReadLoop()
mainHandler.post {
result.success(null)
}
} catch (error: Exception) {
closeUsbConnection()
mainHandler.post {
result.error("usb_connect_failed", error.message, null)
}
}
}
}
private fun resolvePortConfig(device: UsbDevice): PortConfig? {
var preferredDataInterface: UsbInterface? = null
var preferredInEndpoint: UsbEndpoint? = null
var preferredOutEndpoint: UsbEndpoint? = null
var fallbackDataInterface: UsbInterface? = null
var fallbackInEndpoint: UsbEndpoint? = null
var fallbackOutEndpoint: UsbEndpoint? = null
var preferredControlInterface: UsbInterface? = null
for (interfaceIndex in 0 until device.interfaceCount) {
val usbInterface = device.getInterface(interfaceIndex)
var inEndpoint: UsbEndpoint? = null
var outEndpoint: UsbEndpoint? = null
for (endpointIndex in 0 until usbInterface.endpointCount) {
val endpoint = usbInterface.getEndpoint(endpointIndex)
if (endpoint.type != UsbConstants.USB_ENDPOINT_XFER_BULK) {
continue
}
when (endpoint.direction) {
UsbConstants.USB_DIR_IN -> if (inEndpoint == null) inEndpoint = endpoint
UsbConstants.USB_DIR_OUT -> if (outEndpoint == null) outEndpoint = endpoint
}
}
val hasDataPair = inEndpoint != null && outEndpoint != null
when {
usbInterface.interfaceClass == UsbConstants.USB_CLASS_COMM &&
preferredControlInterface == null -> {
preferredControlInterface = usbInterface
}
hasDataPair &&
usbInterface.interfaceClass == UsbConstants.USB_CLASS_CDC_DATA -> {
preferredDataInterface = usbInterface
preferredInEndpoint = inEndpoint
preferredOutEndpoint = outEndpoint
}
hasDataPair && fallbackDataInterface == null -> {
fallbackDataInterface = usbInterface
fallbackInEndpoint = inEndpoint
fallbackOutEndpoint = outEndpoint
}
}
}
val dataInterface = preferredDataInterface ?: fallbackDataInterface ?: return null
val inEndpoint = preferredInEndpoint ?: fallbackInEndpoint ?: return null
val outEndpoint = preferredOutEndpoint ?: fallbackOutEndpoint ?: return null
return PortConfig(preferredControlInterface, dataInterface, inEndpoint, outEndpoint)
}
private fun configureDevice(
connection: UsbDeviceConnection,
config: PortConfig,
baudRate: Int,
) {
val control = config.controlInterface ?: return
val lineCoding =
byteArrayOf(
(baudRate and 0xFF).toByte(),
((baudRate shr 8) and 0xFF).toByte(),
((baudRate shr 16) and 0xFF).toByte(),
((baudRate shr 24) and 0xFF).toByte(),
0, // stop bits: 1
0, // parity: none
8, // data bits
)
val lineCodingResult =
connection.controlTransfer(
UsbConstants.USB_DIR_OUT or
UsbConstants.USB_TYPE_CLASS or
usbRecipientInterface,
0x20,
0,
control.id,
lineCoding,
lineCoding.size,
1000,
)
if (lineCodingResult < 0) {
throw IllegalStateException("Failed to configure USB line coding")
}
val controlLineResult =
connection.controlTransfer(
UsbConstants.USB_DIR_OUT or
UsbConstants.USB_TYPE_CLASS or
usbRecipientInterface,
0x22,
0x0001, // DTR on, RTS off
control.id,
null,
0,
1000,
)
if (controlLineResult < 0) {
throw IllegalStateException("Failed to configure USB control line state")
}
}
private fun startReadLoop() {
val connection = usbConnection ?: return
val endpoint = usbInEndpoint ?: return
isReading = true
readThread =
Thread({
val packetSize = endpoint.maxPacketSize.coerceAtLeast(64)
val buffer = ByteArray(packetSize * 4)
try {
while (isReading) {
val bytesRead = connection.bulkTransfer(endpoint, buffer, buffer.size, 250)
if (!isReading) {
break
}
if (bytesRead <= 0) {
continue
}
val packet = buffer.copyOf(bytesRead)
mainHandler.post {
eventSink?.success(packet)
}
}
} catch (error: Exception) {
if (isReading) {
mainHandler.post {
eventSink?.error(
"usb_io_error",
error.message ?: "USB serial I/O error",
null,
)
}
scheduleCloseUsbConnection()
}
}
}, "MeshCoreUsbRead").also { thread ->
thread.isDaemon = true
thread.start()
}
}
private fun writeToDevice(data: ByteArray) {
val connection = usbConnection ?: throw IllegalStateException("USB connection missing")
val endpoint = usbOutEndpoint ?: throw IllegalStateException("USB output endpoint missing")
var offset = 0
val maxPacketSize = endpoint.maxPacketSize.coerceAtLeast(64)
while (offset < data.size) {
val chunkSize = minOf(maxPacketSize, data.size - offset)
val chunk = data.copyOfRange(offset, offset + chunkSize)
val bytesWritten = connection.bulkTransfer(endpoint, chunk, chunkSize, 1000)
if (bytesWritten != chunkSize) {
throw IllegalStateException("Short USB write: wrote $bytesWritten of $chunkSize bytes")
}
offset += chunkSize
}
}
private fun scheduleCloseUsbConnection(onComplete: (() -> Unit)? = null) {
usbIoExecutor.execute {
closeUsbConnection()
if (onComplete != null) {
mainHandler.post(onComplete)
}
}
}
@Synchronized
private fun closeUsbConnection() {
isReading = false
readThread?.interrupt()
if (readThread != null && readThread !== Thread.currentThread()) {
try {
readThread?.join(300)
} catch (_: InterruptedException) {
Thread.currentThread().interrupt()
}
}
readThread = null
val connection = usbConnection
val claimedControl = controlInterface
val claimedData = dataInterface
usbInEndpoint = null
usbOutEndpoint = null
controlInterface = null
dataInterface = null
usbConnection = null
if (connection != null) {
if (claimedControl != null) {
try {
connection.releaseInterface(claimedControl)
} catch (_: Exception) {
}
}
if (claimedData != null && claimedData.id != claimedControl?.id) {
try {
connection.releaseInterface(claimedData)
} catch (_: Exception) {
}
}
try {
connection.close()
} catch (_: Exception) {
}
}
connectedDeviceName = null
}
private fun handleUsbDetached(intent: Intent) {
val detachedDevice =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)
}
val detachedName = detachedDevice?.deviceName ?: return
if (pendingConnectPortName == detachedName) {
pendingConnectResult?.error(
"usb_device_detached",
"USB device was removed before the connection completed",
null,
)
pendingConnectResult = null
pendingConnectPortName = null
}
if (connectedDeviceName == detachedName) {
scheduleCloseUsbConnection {
eventSink?.error(
"usb_device_detached",
"USB device was disconnected",
null,
)
}
}
}
private fun pendingIntentFlags(): Int {
var flags = PendingIntent.FLAG_UPDATE_CURRENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
flags = flags or PendingIntent.FLAG_MUTABLE
}
return flags
}
}
-1
View File
@@ -2,7 +2,6 @@ allprojects {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven(url = "https://jitpack.io")
} }
} }
+187 -118
View File
@@ -166,6 +166,9 @@ class MeshCoreConnector extends ChangeNotifier {
bool _hasReceivedDeviceInfo = false; bool _hasReceivedDeviceInfo = false;
bool _pendingInitialChannelSync = false; bool _pendingInitialChannelSync = false;
bool _pendingInitialContactsSync = false; bool _pendingInitialContactsSync = false;
bool _bleInitialSyncStarted = false;
bool _pendingDeferredChannelSyncAfterContacts = false;
bool _webInitialHandshakeRequestSent = false;
bool _preserveContactsOnRefresh = false; bool _preserveContactsOnRefresh = false;
static const int _defaultMaxContacts = 32; static const int _defaultMaxContacts = 32;
static const int _defaultMaxChannels = 8; static const int _defaultMaxChannels = 8;
@@ -364,6 +367,8 @@ class MeshCoreConnector extends ChangeNotifier {
} }
} }
// Re-sort after merging persisted and in-memory messages so the
// conversation window remains stable after optimistic inserts.
mergedMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); mergedMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
final windowedMergedMessages = mergedMessages.length > _messageWindowSize final windowedMergedMessages = mergedMessages.length > _messageWindowSize
? mergedMessages.sublist(mergedMessages.length - _messageWindowSize) ? mergedMessages.sublist(mergedMessages.length - _messageWindowSize)
@@ -820,6 +825,76 @@ class MeshCoreConnector extends ChangeNotifier {
_usbSerialService.setRequestPortLabel(label); _usbSerialService.setRequestPortLabel(label);
} }
Future<void> connectUsb({
required String portName,
int baudRate = 115200,
}) async {
if (_state == MeshCoreConnectionState.connecting ||
_state == MeshCoreConnectionState.connected) {
return;
}
_activeTransport = MeshCoreTransportType.bluetooth;
_activeUsbPortKey = null;
_activeUsbPortLabel = null;
await stopScan();
_cancelReconnectTimer();
_manualDisconnect = false;
_resetConnectionHandshakeState();
_activeTransport = MeshCoreTransportType.usb;
_activeUsbPortKey = portName;
_activeUsbPortLabel = portName;
_setState(MeshCoreConnectionState.connecting);
try {
await _usbFrameSubscription?.cancel();
_usbFrameSubscription = null;
await _usbSerialService.connect(portName: portName, baudRate: baudRate);
_activeUsbPortKey = _usbSerialService.activePortKey ?? _activeUsbPortKey;
_activeUsbPortLabel =
_usbSerialService.activePortDisplayLabel ?? _activeUsbPortLabel;
notifyListeners();
if (PlatformInfo.isWeb) {
await stopScan();
}
await Future<void>.delayed(const Duration(milliseconds: 200));
_usbFrameSubscription = _usbSerialService.frameStream.listen(
_handleFrame,
onError: (error, stackTrace) {
_appDebugLogService?.error('USB transport error: $error', tag: 'USB');
unawaited(disconnect(manual: false));
},
onDone: () {
unawaited(disconnect(manual: false));
},
);
_setState(MeshCoreConnectionState.connected);
_pendingInitialChannelSync = true;
await _requestDeviceInfo();
_startBatteryPolling();
var gotSelfInfo = await _waitForSelfInfo(
timeout: const Duration(seconds: 3),
);
if (!gotSelfInfo) {
await refreshDeviceInfo();
gotSelfInfo = await _waitForSelfInfo(
timeout: const Duration(seconds: 3),
);
}
if (!gotSelfInfo) {
throw StateError('Timed out waiting for SELF_INFO during connect');
}
await syncTime();
} catch (error) {
_appDebugLogService?.error('USB connection error: $error', tag: 'USB');
await disconnect(manual: false);
rethrow;
}
}
Future<void> connect(BluetoothDevice device, {String? displayName}) async { Future<void> connect(BluetoothDevice device, {String? displayName}) async {
if (_state == MeshCoreConnectionState.connecting || if (_state == MeshCoreConnectionState.connecting ||
_state == MeshCoreConnectionState.connected) { _state == MeshCoreConnectionState.connected) {
@@ -844,6 +919,7 @@ class MeshCoreConnector extends ChangeNotifier {
_lastDeviceDisplayName = _deviceDisplayName; _lastDeviceDisplayName = _deviceDisplayName;
_manualDisconnect = false; _manualDisconnect = false;
_cancelReconnectTimer(); _cancelReconnectTimer();
_bleInitialSyncStarted = false;
if (PlatformInfo.isWeb) { if (PlatformInfo.isWeb) {
_resetConnectionHandshakeState(); _resetConnectionHandshakeState();
} }
@@ -856,6 +932,10 @@ class MeshCoreConnector extends ChangeNotifier {
'Starting connect to $connectLabel', 'Starting connect to $connectLabel',
tag: 'BLE Connect', tag: 'BLE Connect',
); );
await _connectionSubscription?.cancel();
_connectionSubscription = null;
await _notifySubscription?.cancel();
_notifySubscription = null;
_connectionSubscription = device.connectionState.listen((state) { _connectionSubscription = device.connectionState.listen((state) {
if (state == BluetoothConnectionState.disconnected && isConnected) { if (state == BluetoothConnectionState.disconnected && isConnected) {
_handleDisconnection(); _handleDisconnection();
@@ -899,6 +979,8 @@ class MeshCoreConnector extends ChangeNotifier {
); );
if (PlatformInfo.isWeb && if (PlatformInfo.isWeb &&
error.toString().contains('GATT Server is disconnected')) { error.toString().contains('GATT Server is disconnected')) {
// Chrome Web Bluetooth intermittently disconnects between connect()
// and service discovery; retry once to recover that transient state.
_appDebugLogService?.warn( _appDebugLogService?.warn(
'retrying service discovery after transient web disconnect', 'retrying service discovery after transient web disconnect',
tag: 'BLE Connect', tag: 'BLE Connect',
@@ -995,42 +1077,7 @@ class MeshCoreConnector extends ChangeNotifier {
_hasReceivedDeviceInfo = false; _hasReceivedDeviceInfo = false;
_pendingInitialChannelSync = true; _pendingInitialChannelSync = true;
} }
unawaited(Future<void>.microtask(() => _startBleInitialSync()));
await _requestDeviceInfo();
_startBatteryPolling();
if (PlatformInfo.isWeb &&
_activeTransport == MeshCoreTransportType.bluetooth) {
// Chrome's Web Bluetooth stack commonly delays incoming notifications
// until the non-blocking notify setup settles. Avoid stacking extra
// startup writes while that is happening. Defer the clock sync until
// the connection has had time to settle.
unawaited(
Future<void>(() async {
await Future<void>.delayed(const Duration(seconds: 5));
if (!isConnected ||
!PlatformInfo.isWeb ||
_activeTransport != MeshCoreTransportType.bluetooth) {
return;
}
await syncTime();
}),
);
} else {
final gotSelfInfo = await _waitForSelfInfo(
timeout: const Duration(seconds: 3),
);
if (!gotSelfInfo) {
await refreshDeviceInfo();
await _waitForSelfInfo(timeout: const Duration(seconds: 3));
}
unawaited(syncTime());
}
// Fetch channels so we can track unread counts for incoming messages
if (!_shouldGateInitialChannelSync) {
unawaited(getChannels());
}
} catch (e) { } catch (e) {
_appDebugLogService?.error('Connection error: $e', tag: 'BLE Connect'); _appDebugLogService?.error('Connection error: $e', tag: 'BLE Connect');
await disconnect(manual: false); await disconnect(manual: false);
@@ -1038,76 +1085,6 @@ class MeshCoreConnector extends ChangeNotifier {
} }
} }
Future<void> connectUsb({
required String portName,
int baudRate = 115200,
}) async {
if (_state == MeshCoreConnectionState.connecting ||
_state == MeshCoreConnectionState.connected) {
return;
}
_activeTransport = MeshCoreTransportType.bluetooth;
_activeUsbPortKey = null;
_activeUsbPortLabel = null;
await stopScan();
_cancelReconnectTimer();
_manualDisconnect = false;
_resetConnectionHandshakeState();
_activeTransport = MeshCoreTransportType.usb;
_activeUsbPortKey = portName;
_activeUsbPortLabel = portName;
_setState(MeshCoreConnectionState.connecting);
try {
await _usbFrameSubscription?.cancel();
_usbFrameSubscription = null;
await _usbSerialService.connect(portName: portName, baudRate: baudRate);
_activeUsbPortKey = _usbSerialService.activePortKey ?? _activeUsbPortKey;
_activeUsbPortLabel =
_usbSerialService.activePortDisplayLabel ?? _activeUsbPortLabel;
notifyListeners();
if (PlatformInfo.isWeb) {
await stopScan();
}
await Future<void>.delayed(const Duration(milliseconds: 200));
_usbFrameSubscription = _usbSerialService.frameStream.listen(
_handleFrame,
onError: (error, stackTrace) {
_appDebugLogService?.error('USB transport error: $error', tag: 'USB');
unawaited(disconnect(manual: false));
},
onDone: () {
unawaited(disconnect(manual: false));
},
);
_setState(MeshCoreConnectionState.connected);
_pendingInitialChannelSync = true;
await _requestDeviceInfo();
_startBatteryPolling();
var gotSelfInfo = await _waitForSelfInfo(
timeout: const Duration(seconds: 3),
);
if (!gotSelfInfo) {
await refreshDeviceInfo();
gotSelfInfo = await _waitForSelfInfo(
timeout: const Duration(seconds: 3),
);
}
if (!gotSelfInfo) {
throw StateError('Timed out waiting for SELF_INFO during connect');
}
await syncTime();
} catch (error) {
_appDebugLogService?.error('USB connection error: $error', tag: 'USB');
await disconnect(manual: false);
rethrow;
}
}
Future<bool> _waitForSelfInfo({required Duration timeout}) async { Future<bool> _waitForSelfInfo({required Duration timeout}) async {
if (_selfPublicKey != null) return true; if (_selfPublicKey != null) return true;
if (!isConnected) return false; if (!isConnected) return false;
@@ -1139,17 +1116,60 @@ class MeshCoreConnector extends ChangeNotifier {
return result; return result;
} }
Future<void> _startBleInitialSync() async {
if (_bleInitialSyncStarted ||
!isConnected ||
_activeTransport != MeshCoreTransportType.bluetooth) {
return;
}
_bleInitialSyncStarted = true;
await _requestDeviceInfo();
_startBatteryPolling();
if (PlatformInfo.isWeb) {
// Keep Web BLE startup writes light while notifications settle.
unawaited(
Future<void>(() async {
await Future<void>.delayed(const Duration(seconds: 5));
if (!isConnected ||
!PlatformInfo.isWeb ||
_activeTransport != MeshCoreTransportType.bluetooth) {
return;
}
await syncTime();
}),
);
return;
}
final gotSelfInfo = await _waitForSelfInfo(
timeout: const Duration(seconds: 3),
);
if (!gotSelfInfo) {
await refreshDeviceInfo();
await _waitForSelfInfo(timeout: const Duration(seconds: 3));
}
unawaited(syncTime());
_pendingDeferredChannelSyncAfterContacts = true;
}
void _resetConnectionHandshakeState() { void _resetConnectionHandshakeState() {
_selfPublicKey = null; _selfPublicKey = null;
_selfName = null; _selfName = null;
_selfLatitude = null; _selfLatitude = null;
_selfLongitude = null; _selfLongitude = null;
_awaitingSelfInfo = false; _awaitingSelfInfo = false;
_webInitialHandshakeRequestSent = false;
_selfInfoRetryTimer?.cancel(); _selfInfoRetryTimer?.cancel();
_selfInfoRetryTimer = null; _selfInfoRetryTimer = null;
_hasReceivedDeviceInfo = false; _hasReceivedDeviceInfo = false;
_pendingInitialChannelSync = false; _pendingInitialChannelSync = false;
_pendingInitialContactsSync = false; _pendingInitialContactsSync = false;
_bleInitialSyncStarted = false;
_pendingDeferredChannelSyncAfterContacts = false;
_webInitialHandshakeRequestSent = false;
} }
bool get _shouldAutoReconnect => bool get _shouldAutoReconnect =>
@@ -1205,6 +1225,14 @@ class MeshCoreConnector extends ChangeNotifier {
Future<void> disconnect({bool manual = true}) async { Future<void> disconnect({bool manual = true}) async {
if (_state == MeshCoreConnectionState.disconnecting) return; if (_state == MeshCoreConnectionState.disconnecting) return;
final transportAtDisconnect = _activeTransport; final transportAtDisconnect = _activeTransport;
final transportLabel = transportAtDisconnect == MeshCoreTransportType.usb
? 'USB'
: 'BLE';
_appDebugLogService?.info(
'Starting disconnect transport=$transportLabel manual=$manual',
tag: 'Connection',
);
if (manual) { if (manual) {
_manualDisconnect = true; _manualDisconnect = true;
@@ -1280,6 +1308,10 @@ class MeshCoreConnector extends ChangeNotifier {
_activeUsbPortLabel = null; _activeUsbPortLabel = null;
_setState(MeshCoreConnectionState.disconnected); _setState(MeshCoreConnectionState.disconnected);
_appDebugLogService?.info(
'Disconnect complete transport=$transportLabel manual=$manual',
tag: 'Connection',
);
if (!manual && transportAtDisconnect == MeshCoreTransportType.bluetooth) { if (!manual && transportAtDisconnect == MeshCoreTransportType.bluetooth) {
_scheduleReconnect(); _scheduleReconnect();
} }
@@ -1345,7 +1377,18 @@ class MeshCoreConnector extends ChangeNotifier {
Future<void> refreshDeviceInfo() async { Future<void> refreshDeviceInfo() async {
if (!isConnected) return; if (!isConnected) return;
if (PlatformInfo.isWeb &&
_activeTransport == MeshCoreTransportType.bluetooth &&
_webInitialHandshakeRequestSent &&
_selfPublicKey == null) {
return;
}
_awaitingSelfInfo = true; _awaitingSelfInfo = true;
if (PlatformInfo.isWeb &&
_activeTransport == MeshCoreTransportType.bluetooth &&
_selfPublicKey == null) {
_webInitialHandshakeRequestSent = true;
}
await sendFrame(buildDeviceQueryFrame()); await sendFrame(buildDeviceQueryFrame());
await sendFrame(buildAppStartFrame()); await sendFrame(buildAppStartFrame());
await requestBatteryStatus(force: true); await requestBatteryStatus(force: true);
@@ -1356,7 +1399,18 @@ class MeshCoreConnector extends ChangeNotifier {
Future<void> _requestDeviceInfo() async { Future<void> _requestDeviceInfo() async {
if (!isConnected || _awaitingSelfInfo) return; if (!isConnected || _awaitingSelfInfo) return;
if (PlatformInfo.isWeb &&
_activeTransport == MeshCoreTransportType.bluetooth &&
_webInitialHandshakeRequestSent &&
_selfPublicKey == null) {
return;
}
_awaitingSelfInfo = true; _awaitingSelfInfo = true;
if (PlatformInfo.isWeb &&
_activeTransport == MeshCoreTransportType.bluetooth &&
_selfPublicKey == null) {
_webInitialHandshakeRequestSent = true;
}
await sendFrame(buildDeviceQueryFrame()); await sendFrame(buildDeviceQueryFrame());
await sendFrame(buildAppStartFrame()); await sendFrame(buildAppStartFrame());
await sendFrame(buildGetCustomVarsFrame()); await sendFrame(buildGetCustomVarsFrame());
@@ -2183,6 +2237,12 @@ class MeshCoreConnector extends ChangeNotifier {
_pendingQueueSync = false; _pendingQueueSync = false;
unawaited(syncQueuedMessages(force: true)); unawaited(syncQueuedMessages(force: true));
} }
if (_pendingDeferredChannelSyncAfterContacts &&
(_activeTransport == MeshCoreTransportType.bluetooth ||
_activeTransport == MeshCoreTransportType.usb)) {
_pendingDeferredChannelSyncAfterContacts = false;
unawaited(getChannels());
}
break; break;
case respCodeContactMsgRecv: case respCodeContactMsgRecv:
case respCodeContactMsgRecvV3: case respCodeContactMsgRecvV3:
@@ -2294,6 +2354,8 @@ class MeshCoreConnector extends ChangeNotifier {
// [58+] = node_name // [58+] = node_name
if (frame.length < 4 + pubKeySize) return; if (frame.length < 4 + pubKeySize) return;
final wasAwaitingSelfInfo = _awaitingSelfInfo;
_currentTxPower = frame[2]; _currentTxPower = frame[2];
_maxTxPower = frame[3]; _maxTxPower = frame[3];
_selfPublicKey = Uint8List.fromList(frame.sublist(4, 4 + pubKeySize)); _selfPublicKey = Uint8List.fromList(frame.sublist(4, 4 + pubKeySize));
@@ -2325,15 +2387,25 @@ class MeshCoreConnector extends ChangeNotifier {
_selfInfoRetryTimer = null; _selfInfoRetryTimer = null;
notifyListeners(); notifyListeners();
if (PlatformInfo.isWeb &&
_activeTransport == MeshCoreTransportType.bluetooth &&
!wasAwaitingSelfInfo) {
return;
}
// Auto-fetch contacts after getting self info. On web BLE, defer this // Auto-fetch contacts after getting self info. On web BLE, defer this
// until after channel 0 so startup writes stay serialized. // until after channel 0 so startup writes stay serialized.
if (PlatformInfo.isWeb && if (PlatformInfo.isWeb &&
_activeTransport == MeshCoreTransportType.bluetooth) { _activeTransport == MeshCoreTransportType.bluetooth) {
_pendingInitialContactsSync = true; _pendingInitialContactsSync = true;
} else if (_activeTransport == MeshCoreTransportType.usb) {
_pendingDeferredChannelSyncAfterContacts = true;
getContacts();
} else { } else {
getContacts(); getContacts();
} }
if (_shouldGateInitialChannelSync) { if (_shouldGateInitialChannelSync &&
_activeTransport != MeshCoreTransportType.usb) {
_maybeStartInitialChannelSync(); _maybeStartInitialChannelSync();
} }
} }
@@ -2367,6 +2439,7 @@ class MeshCoreConnector extends ChangeNotifier {
unawaited(loadChannelSettings(maxChannels: nextMaxChannels)); unawaited(loadChannelSettings(maxChannels: nextMaxChannels));
unawaited(loadAllChannelMessages(maxChannels: nextMaxChannels)); unawaited(loadAllChannelMessages(maxChannels: nextMaxChannels));
if (isConnected && if (isConnected &&
_selfPublicKey != null &&
(!_shouldGateInitialChannelSync || !_pendingInitialChannelSync)) { (!_shouldGateInitialChannelSync || !_pendingInitialChannelSync)) {
unawaited(getChannels(maxChannels: nextMaxChannels)); unawaited(getChannels(maxChannels: nextMaxChannels));
} }
@@ -3524,17 +3597,13 @@ class MeshCoreConnector extends ChangeNotifier {
// For 1:1 chats, sender is implicit (null) // For 1:1 chats, sender is implicit (null)
String? senderName; String? senderName;
if (isRoomServer && !msg.isOutgoing) { if (isRoomServer && !msg.isOutgoing) {
// Treat a missing room-contact key as unknown instead of matching every final senderContact = _contacts.cast<Contact?>().firstWhere(
// contact via an empty prefix. (c) =>
if (msg.fourByteRoomContactKey.length == 4) { c != null &&
final senderContact = _contacts.cast<Contact?>().firstWhere( _matchesPrefix(c.publicKey, msg.fourByteRoomContactKey),
(c) => orElse: () => null,
c != null && );
_matchesPrefix(c.publicKey, msg.fourByteRoomContactKey), senderName = senderContact?.name;
orElse: () => null,
);
senderName = senderContact?.name;
}
} else if (isRoomServer && msg.isOutgoing) { } else if (isRoomServer && msg.isOutgoing) {
senderName = selfName; senderName = selfName;
} }
+32
View File
@@ -0,0 +1,32 @@
import 'package:flutter/foundation.dart';
import 'meshcore_connector.dart';
class MeshCoreConnectorUsb {
const MeshCoreConnectorUsb(this.connector);
final MeshCoreConnector connector;
MeshCoreConnectionState get state => connector.state;
MeshCoreTransportType get activeTransport => connector.activeTransport;
String? get activeUsbPortDisplayLabel => connector.activeUsbPortDisplayLabel;
bool get isUsbTransportConnected => connector.isUsbTransportConnected;
void addListener(VoidCallback listener) => connector.addListener(listener);
void removeListener(VoidCallback listener) =>
connector.removeListener(listener);
Future<List<String>> listPorts() => connector.listUsbPorts();
void setRequestPortLabel(String label) {
connector.setUsbRequestPortLabel(label);
}
Future<void> connect({required String portName, int baudRate = 115200}) {
return connector.connectUsb(portName: portName, baudRate: baudRate);
}
Future<void> disconnect({bool manual = true}) {
return connector.disconnect(manual: manual);
}
}
+848 -850
View File
File diff suppressed because it is too large Load Diff
+277 -279
View File
File diff suppressed because it is too large Load Diff
+14 -16
View File
@@ -1,4 +1,4 @@
{ {
"@@locale": "en", "@@locale": "en",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Contacts", "nav_contacts": "Contacts",
@@ -28,7 +28,7 @@
"common_disable": "Disable", "common_disable": "Disable",
"common_reboot": "Reboot", "common_reboot": "Reboot",
"common_loading": "Loading...", "common_loading": "Loading...",
"common_notAvailable": "", "common_notAvailable": "—",
"common_voltageValue": "{volts} V", "common_voltageValue": "{volts} V",
"@common_voltageValue": { "@common_voltageValue": {
"placeholders": { "placeholders": {
@@ -46,8 +46,6 @@
} }
}, },
"scanner_title": "MeshCore Open", "scanner_title": "MeshCore Open",
"connectionChoiceTitle": "Choose your connection method",
"connectionChoiceSubtitle": "Select how you would like to reach your MeshCore device.",
"connectionChoiceUsbLabel": "USB", "connectionChoiceUsbLabel": "USB",
"connectionChoiceBluetoothLabel": "Bluetooth", "connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenTitle": "Connect over USB", "usbScreenTitle": "Connect over USB",
@@ -180,20 +178,20 @@
"appSettings_language": "Language", "appSettings_language": "Language",
"appSettings_languageSystem": "System default", "appSettings_languageSystem": "System default",
"appSettings_languageEn": "English", "appSettings_languageEn": "English",
"appSettings_languageFr": "Français", "appSettings_languageFr": "Français",
"appSettings_languageEs": "Español", "appSettings_languageEs": "Español",
"appSettings_languageDe": "Deutsch", "appSettings_languageDe": "Deutsch",
"appSettings_languagePl": "Polski", "appSettings_languagePl": "Polski",
"appSettings_languageSl": "Slovenščina", "appSettings_languageSl": "Slovenščina",
"appSettings_languagePt": "Português", "appSettings_languagePt": "Português",
"appSettings_languageIt": "Italiano", "appSettings_languageIt": "Italiano",
"appSettings_languageZh": "中文", "appSettings_languageZh": "中文",
"appSettings_languageSv": "Svenska", "appSettings_languageSv": "Svenska",
"appSettings_languageNl": "Nederlands", "appSettings_languageNl": "Nederlands",
"appSettings_languageSk": "Slovenčina", "appSettings_languageSk": "Slovenčina",
"appSettings_languageBg": "Български", "appSettings_languageBg": "Български",
"appSettings_languageRu": "Русский", "appSettings_languageRu": "Русский",
"appSettings_languageUk": "Українська", "appSettings_languageUk": "Українська",
"appSettings_enableMessageTracing": "Enable Message Tracing", "appSettings_enableMessageTracing": "Enable Message Tracing",
"appSettings_enableMessageTracingSubtitle": "Show detailed routing and timing metadata for messages", "appSettings_enableMessageTracingSubtitle": "Show detailed routing and timing metadata for messages",
"appSettings_notifications": "Notifications", "appSettings_notifications": "Notifications",
@@ -1341,7 +1339,7 @@
} }
} }
}, },
"telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F",
"@telemetry_temperatureValue": { "@telemetry_temperatureValue": {
"placeholders": { "placeholders": {
"celsius": { "celsius": {
@@ -1391,7 +1389,7 @@
"channelPath_repeatsLabel": "Repeats", "channelPath_repeatsLabel": "Repeats",
"channelPath_pathLabel": "Path {index}", "channelPath_pathLabel": "Path {index}",
"channelPath_observedLabel": "Observed", "channelPath_observedLabel": "Observed",
"channelPath_observedPathTitle": "Observed path {index} {hops}", "channelPath_observedPathTitle": "Observed path {index} • {hops}",
"@channelPath_observedPathTitle": { "@channelPath_observedPathTitle": {
"placeholders": { "placeholders": {
"index": { "index": {
@@ -1466,7 +1464,7 @@
}, },
"channelPath_pathLabelTitle": "Path", "channelPath_pathLabelTitle": "Path",
"channelPath_observedPathHeader": "Observed Path", "channelPath_observedPathHeader": "Observed Path",
"channelPath_selectedPathLabel": "{label} {prefixes}", "channelPath_selectedPathLabel": "{label} • {prefixes}",
"@channelPath_selectedPathLabel": { "@channelPath_selectedPathLabel": {
"placeholders": { "placeholders": {
"label": { "label": {
+341 -343
View File
File diff suppressed because it is too large Load Diff
+509 -511
View File
File diff suppressed because it is too large Load Diff
+96 -98
View File
@@ -1,4 +1,4 @@
{ {
"channels_channelDeleteFailed": "Impossibile eliminare il canale \"{name}\"", "channels_channelDeleteFailed": "Impossibile eliminare il canale \"{name}\"",
"@channels_channelDeleteFailed": { "@channels_channelDeleteFailed": {
"placeholders": { "placeholders": {
@@ -35,7 +35,7 @@
"common_disable": "Disattivare", "common_disable": "Disattivare",
"common_reboot": "Riavvia", "common_reboot": "Riavvia",
"common_loading": "Caricamento...", "common_loading": "Caricamento...",
"common_notAvailable": "", "common_notAvailable": "—",
"common_voltageValue": "{volts} V", "common_voltageValue": "{volts} V",
"@common_voltageValue": { "@common_voltageValue": {
"placeholders": { "placeholders": {
@@ -98,11 +98,11 @@
"settings_locationInvalid": "Latitudine o longitudine non valida.", "settings_locationInvalid": "Latitudine o longitudine non valida.",
"settings_latitude": "Latitudine", "settings_latitude": "Latitudine",
"settings_longitude": "Longitudine", "settings_longitude": "Longitudine",
"settings_privacyMode": "Modalità Privacy", "settings_privacyMode": "Modalità Privacy",
"settings_privacyModeSubtitle": "Nascondere nome/luogo negli annunci", "settings_privacyModeSubtitle": "Nascondere nome/luogo negli annunci",
"settings_privacyModeToggle": "Attiva la modalità privacy per nascondere il tuo nome e la tua posizione negli annunci.", "settings_privacyModeToggle": "Attiva la modalità privacy per nascondere il tuo nome e la tua posizione negli annunci.",
"settings_privacyModeEnabled": "Modalità privacy abilitata", "settings_privacyModeEnabled": "Modalità privacy abilitata",
"settings_privacyModeDisabled": "Modalità privacy disabilitata", "settings_privacyModeDisabled": "Modalità privacy disabilitata",
"settings_actions": "Azioni", "settings_actions": "Azioni",
"settings_sendAdvertisement": "Invia Annuncio", "settings_sendAdvertisement": "Invia Annuncio",
"settings_sendAdvertisementSubtitle": "Presenza trasmessa ora", "settings_sendAdvertisementSubtitle": "Presenza trasmessa ora",
@@ -165,18 +165,18 @@
"appSettings_language": "Lingua", "appSettings_language": "Lingua",
"appSettings_languageSystem": "Predefinito di sistema", "appSettings_languageSystem": "Predefinito di sistema",
"appSettings_languageEn": "English", "appSettings_languageEn": "English",
"appSettings_languageFr": "Français", "appSettings_languageFr": "Français",
"appSettings_languageEs": "Español", "appSettings_languageEs": "Español",
"appSettings_languageDe": "Deutsch", "appSettings_languageDe": "Deutsch",
"appSettings_languagePl": "Polski", "appSettings_languagePl": "Polski",
"appSettings_languageSl": "Slovenščina", "appSettings_languageSl": "Slovenščina",
"appSettings_languagePt": "Português", "appSettings_languagePt": "Português",
"appSettings_languageIt": "Italiano", "appSettings_languageIt": "Italiano",
"appSettings_languageZh": "中文", "appSettings_languageZh": "中文",
"appSettings_languageSv": "Svenska", "appSettings_languageSv": "Svenska",
"appSettings_languageNl": "Nederlands", "appSettings_languageNl": "Nederlands",
"appSettings_languageSk": "Slovenčina", "appSettings_languageSk": "Slovenčina",
"appSettings_languageBg": "Български", "appSettings_languageBg": "Български",
"appSettings_notifications": "Notifiche", "appSettings_notifications": "Notifiche",
"appSettings_enableNotifications": "Abilita Notifiche", "appSettings_enableNotifications": "Abilita Notifiche",
"appSettings_enableNotificationsSubtitle": "Ricevi notifiche per messaggi e annunci", "appSettings_enableNotificationsSubtitle": "Ricevi notifiche per messaggi e annunci",
@@ -195,7 +195,7 @@
"appSettings_pathsWillBeCleared": "I percorsi verranno puliti dopo 5 tentativi falliti.", "appSettings_pathsWillBeCleared": "I percorsi verranno puliti dopo 5 tentativi falliti.",
"appSettings_pathsWillNotBeCleared": "I percorsi non verranno eliminati automaticamente.", "appSettings_pathsWillNotBeCleared": "I percorsi non verranno eliminati automaticamente.",
"appSettings_autoRouteRotation": "Rotazione Percorso Automatico", "appSettings_autoRouteRotation": "Rotazione Percorso Automatico",
"appSettings_autoRouteRotationSubtitle": "Alterna tra i percorsi migliori e la modalità alluvione", "appSettings_autoRouteRotationSubtitle": "Alterna tra i percorsi migliori e la modalità alluvione",
"appSettings_autoRouteRotationEnabled": "Rotazione percorso automatico abilitata", "appSettings_autoRouteRotationEnabled": "Rotazione percorso automatico abilitata",
"appSettings_autoRouteRotationDisabled": "Rotazione del percorso automatico disabilitata", "appSettings_autoRouteRotationDisabled": "Rotazione del percorso automatico disabilitata",
"appSettings_battery": "Batteria", "appSettings_battery": "Batteria",
@@ -284,8 +284,8 @@
}, },
"contacts_newGroup": "Nuovo Gruppo", "contacts_newGroup": "Nuovo Gruppo",
"contacts_groupName": "Nome gruppo", "contacts_groupName": "Nome gruppo",
"contacts_groupNameRequired": "Il nome del gruppo è obbligatorio.", "contacts_groupNameRequired": "Il nome del gruppo è obbligatorio.",
"contacts_groupAlreadyExists": "Il gruppo \"{name}\" esiste già.", "contacts_groupAlreadyExists": "Il gruppo \"{name}\" esiste già.",
"@contacts_groupAlreadyExists": { "@contacts_groupAlreadyExists": {
"placeholders": { "placeholders": {
"name": { "name": {
@@ -345,7 +345,7 @@
"channels_muteChannel": "Silenzia canale", "channels_muteChannel": "Silenzia canale",
"channels_unmuteChannel": "Attiva notifiche canale", "channels_unmuteChannel": "Attiva notifiche canale",
"channels_deleteChannel": "Elimina canale", "channels_deleteChannel": "Elimina canale",
"channels_deleteChannelConfirm": "Eliminare \"{name}\"? Non può essere annullato.", "channels_deleteChannelConfirm": "Eliminare \"{name}\"? Non può essere annullato.",
"@channels_deleteChannelConfirm": { "@channels_deleteChannelConfirm": {
"placeholders": { "placeholders": {
"name": { "name": {
@@ -477,7 +477,7 @@
"debugLog_enableInSettings": "Abilita il logging di debug dell'app nelle impostazioni", "debugLog_enableInSettings": "Abilita il logging di debug dell'app nelle impostazioni",
"debugLog_frames": "Frame", "debugLog_frames": "Frame",
"debugLog_rawLogRx": "Log Raw-RX", "debugLog_rawLogRx": "Log Raw-RX",
"debugLog_noBleActivity": "Nessuna attività BLE rilevata ancora.", "debugLog_noBleActivity": "Nessuna attività BLE rilevata ancora.",
"debugFrame_length": "Lunghezza del Frame: {count} byte", "debugFrame_length": "Lunghezza del Frame: {count} byte",
"@debugFrame_length": { "@debugFrame_length": {
"placeholders": { "placeholders": {
@@ -542,11 +542,11 @@
}, },
"debugFrame_hexDump": "Dumpa Esadecimale:", "debugFrame_hexDump": "Dumpa Esadecimale:",
"chat_pathManagement": "Gestione Percorsi", "chat_pathManagement": "Gestione Percorsi",
"chat_routingMode": "Modalità di routing", "chat_routingMode": "Modalità di routing",
"chat_autoUseSavedPath": "Utilizza il percorso salvato", "chat_autoUseSavedPath": "Utilizza il percorso salvato",
"chat_forceFloodMode": "Modalità Inondamento Forzato", "chat_forceFloodMode": "Modalità Inondamento Forzato",
"chat_recentAckPaths": "Percorsi ACK Recenti (tocca per usare):", "chat_recentAckPaths": "Percorsi ACK Recenti (tocca per usare):",
"chat_pathHistoryFull": "La cronologia del percorso è piena. Rimuovi gli elementi per aggiungere nuovi.", "chat_pathHistoryFull": "La cronologia del percorso è piena. Rimuovi gli elementi per aggiungere nuovi.",
"chat_hopSingular": "salta", "chat_hopSingular": "salta",
"chat_hopPlural": "salta", "chat_hopPlural": "salta",
"chat_hopsCount": "{count} {count, plural, =1{salto} other{salti}}", "chat_hopsCount": "{count} {count, plural, =1{salto} other{salti}}",
@@ -559,15 +559,15 @@
}, },
"chat_successes": "successi", "chat_successes": "successi",
"chat_removePath": "Rimuovi percorso", "chat_removePath": "Rimuovi percorso",
"chat_noPathHistoryYet": "Non c'è ancora una cronologia del percorso.\nInvia un messaggio per scoprire i percorsi.", "chat_noPathHistoryYet": "Non c'è ancora una cronologia del percorso.\nInvia un messaggio per scoprire i percorsi.",
"chat_pathActions": "Azioni Percorso:", "chat_pathActions": "Azioni Percorso:",
"chat_setCustomPath": "Imposta Percorso Personalizzato", "chat_setCustomPath": "Imposta Percorso Personalizzato",
"chat_setCustomPathSubtitle": "Specifica manualmente il percorso di routing", "chat_setCustomPathSubtitle": "Specifica manualmente il percorso di routing",
"chat_clearPath": "Cancella Percorso", "chat_clearPath": "Cancella Percorso",
"chat_clearPathSubtitle": "Riprova la scoperta alla prossima invio", "chat_clearPathSubtitle": "Riprova la scoperta alla prossima invio",
"chat_pathCleared": "Percorso sgomberato. Il prossimo messaggio riidentifierà il percorso.", "chat_pathCleared": "Percorso sgomberato. Il prossimo messaggio riidentifierà il percorso.",
"chat_floodModeSubtitle": "Utilizza l'interruttore di routing nella barra delle applicazioni", "chat_floodModeSubtitle": "Utilizza l'interruttore di routing nella barra delle applicazioni",
"chat_floodModeEnabled": "Modalità alluvione abilitata. Disattivala tramite l'icona di routing nella barra in alto.", "chat_floodModeEnabled": "Modalità alluvione abilitata. Disattivala tramite l'icona di routing nella barra in alto.",
"chat_fullPath": "Percorso Completo", "chat_fullPath": "Percorso Completo",
"chat_pathDetailsNotAvailable": "I dettagli del percorso non sono ancora disponibili. Prova a inviare un messaggio per ricaricare.", "chat_pathDetailsNotAvailable": "I dettagli del percorso non sono ancora disponibili. Prova a inviare un messaggio per ricaricare.",
"chat_pathSetHops": "Percorso impostato: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", "chat_pathSetHops": "Percorso impostato: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}",
@@ -660,7 +660,7 @@
"map_sendToChannel": "Invia al canale", "map_sendToChannel": "Invia al canale",
"map_noChannelsAvailable": "Nessun canale disponibile", "map_noChannelsAvailable": "Nessun canale disponibile",
"map_publicLocationShare": "Condividi in una posizione pubblica", "map_publicLocationShare": "Condividi in una posizione pubblica",
"map_publicLocationShareConfirm": "Stai per condividere una posizione in {channelLabel}. Questo canale è pubblico e chiunque abbia la PSK può vederlo.", "map_publicLocationShareConfirm": "Stai per condividere una posizione in {channelLabel}. Questo canale è pubblico e chiunque abbia la PSK può vederlo.",
"@map_publicLocationShareConfirm": { "@map_publicLocationShareConfirm": {
"placeholders": { "placeholders": {
"channelLabel": { "channelLabel": {
@@ -810,13 +810,13 @@
"login_password": "Password", "login_password": "Password",
"login_enterPassword": "Inserisci password", "login_enterPassword": "Inserisci password",
"login_savePassword": "Salva password", "login_savePassword": "Salva password",
"login_savePasswordSubtitle": "La password verrà memorizzata in modo sicuro su questo dispositivo.", "login_savePasswordSubtitle": "La password verrà memorizzata in modo sicuro su questo dispositivo.",
"login_repeaterDescription": "Inserisci la password del ripetitore per accedere alle impostazioni e allo stato.", "login_repeaterDescription": "Inserisci la password del ripetitore per accedere alle impostazioni e allo stato.",
"login_roomDescription": "Inserisci la password della stanza per accedere alle impostazioni e allo stato.", "login_roomDescription": "Inserisci la password della stanza per accedere alle impostazioni e allo stato.",
"login_routing": "Instradamento", "login_routing": "Instradamento",
"login_routingMode": "Modalità di routing", "login_routingMode": "Modalità di routing",
"login_autoUseSavedPath": "Utilizza il percorso salvato", "login_autoUseSavedPath": "Utilizza il percorso salvato",
"login_forceFloodMode": "Modalità Inondamento Forzato", "login_forceFloodMode": "Modalità Inondamento Forzato",
"login_managePaths": "Gestisci Percorsi", "login_managePaths": "Gestisci Percorsi",
"login_login": "Accedi", "login_login": "Accedi",
"login_attempt": "Prova {current}/{max}", "login_attempt": "Prova {current}/{max}",
@@ -838,7 +838,7 @@
} }
} }
}, },
"login_failedMessage": "Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.", "login_failedMessage": "Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.",
"common_reload": "Ricaricare", "common_reload": "Ricaricare",
"common_clear": "Cancella", "common_clear": "Cancella",
"path_currentPath": "Percorso corrente: {path}", "path_currentPath": "Percorso corrente: {path}",
@@ -862,7 +862,7 @@
"path_hexPrefixInstructions": "Inserire i prefissi esadecimali a 2 caratteri per ogni salto, separati da virgole.", "path_hexPrefixInstructions": "Inserire i prefissi esadecimali a 2 caratteri per ogni salto, separati da virgole.",
"path_hexPrefixExample": "Esempio: A1,F2,3C (ogni nodo utilizza il primo byte della sua chiave pubblica)", "path_hexPrefixExample": "Esempio: A1,F2,3C (ogni nodo utilizza il primo byte della sua chiave pubblica)",
"path_labelHexPrefixes": "Prefisso esadecimale (percorso)", "path_labelHexPrefixes": "Prefisso esadecimale (percorso)",
"path_helperMaxHops": "Massimo 64 salti. Ogni prefisso è composto da 2 caratteri esadecimali (1 byte)", "path_helperMaxHops": "Massimo 64 salti. Ogni prefisso è composto da 2 caratteri esadecimali (1 byte)",
"path_selectFromContacts": "Seleziona da contatti:", "path_selectFromContacts": "Seleziona da contatti:",
"path_noRepeatersFound": "Non sono stati trovati ripetitori o server di stanza.", "path_noRepeatersFound": "Non sono stati trovati ripetitori o server di stanza.",
"path_customPathsRequire": "I percorsi personalizzati richiedono salti intermedi che possono inoltrare messaggi.", "path_customPathsRequire": "I percorsi personalizzati richiedono salti intermedi che possono inoltrare messaggi.",
@@ -874,7 +874,7 @@
} }
} }
}, },
"path_tooLong": "Il percorso è troppo lungo. Massimo 64 salti consentiti.", "path_tooLong": "Il percorso è troppo lungo. Massimo 64 salti consentiti.",
"path_setPath": "Imposta Percorso", "path_setPath": "Imposta Percorso",
"repeater_management": "Gestione Ripetitori", "repeater_management": "Gestione Ripetitori",
"repeater_managementTools": "Strumenti di Gestione", "repeater_managementTools": "Strumenti di Gestione",
@@ -887,9 +887,9 @@
"repeater_settings": "Impostazioni", "repeater_settings": "Impostazioni",
"repeater_settingsSubtitle": "Configura i parametri del ripetitore", "repeater_settingsSubtitle": "Configura i parametri del ripetitore",
"repeater_statusTitle": "Stato del Ripetitore", "repeater_statusTitle": "Stato del Ripetitore",
"repeater_routingMode": "Modalità di routing", "repeater_routingMode": "Modalità di routing",
"repeater_autoUseSavedPath": "Percorso salvato automatico", "repeater_autoUseSavedPath": "Percorso salvato automatico",
"repeater_forceFloodMode": "Modalità Inondamento Forzato", "repeater_forceFloodMode": "Modalità Inondamento Forzato",
"repeater_pathManagement": "Gestione dei percorsi", "repeater_pathManagement": "Gestione dei percorsi",
"repeater_refresh": "Aggiorna", "repeater_refresh": "Aggiorna",
"repeater_statusRequestTimeout": "Richiesta stato scaduta.", "repeater_statusRequestTimeout": "Richiesta stato scaduta.",
@@ -904,7 +904,7 @@
"repeater_systemInformation": "Informazioni di sistema", "repeater_systemInformation": "Informazioni di sistema",
"repeater_battery": "Batteria", "repeater_battery": "Batteria",
"repeater_clockAtLogin": "Orologio (all'accesso)", "repeater_clockAtLogin": "Orologio (all'accesso)",
"repeater_uptime": "Disponibilità", "repeater_uptime": "Disponibilità",
"repeater_queueLength": "Lunghezza della coda", "repeater_queueLength": "Lunghezza della coda",
"repeater_debugFlags": "Impostazioni Debug", "repeater_debugFlags": "Impostazioni Debug",
"repeater_radioStatistics": "Statistiche Radio", "repeater_radioStatistics": "Statistiche Radio",
@@ -1007,10 +1007,10 @@
"repeater_packetForwardingSubtitle": "Abilita il ripetitore per inoltrare i pacchetti", "repeater_packetForwardingSubtitle": "Abilita il ripetitore per inoltrare i pacchetti",
"repeater_guestAccess": "Accesso Ospite", "repeater_guestAccess": "Accesso Ospite",
"repeater_guestAccessSubtitle": "Consenti l'accesso ospite in sola lettura", "repeater_guestAccessSubtitle": "Consenti l'accesso ospite in sola lettura",
"repeater_privacyMode": "Modalità Privacy", "repeater_privacyMode": "Modalità Privacy",
"repeater_privacyModeSubtitle": "Nascondere nome/luogo negli annunci", "repeater_privacyModeSubtitle": "Nascondere nome/luogo negli annunci",
"repeater_advertisementSettings": "Impostazioni Annuncio", "repeater_advertisementSettings": "Impostazioni Annuncio",
"repeater_localAdvertInterval": "Intervallo Pubblicità Locale", "repeater_localAdvertInterval": "Intervallo Pubblicità Locale",
"repeater_localAdvertIntervalMinutes": "{minutes} minuti", "repeater_localAdvertIntervalMinutes": "{minutes} minuti",
"@repeater_localAdvertIntervalMinutes": { "@repeater_localAdvertIntervalMinutes": {
"placeholders": { "placeholders": {
@@ -1019,7 +1019,7 @@
} }
} }
}, },
"repeater_floodAdvertInterval": "Intervallo Pubblicità Inondazione", "repeater_floodAdvertInterval": "Intervallo Pubblicità Inondazione",
"repeater_floodAdvertIntervalHours": "{hours} ore", "repeater_floodAdvertIntervalHours": "{hours} ore",
"@repeater_floodAdvertIntervalHours": { "@repeater_floodAdvertIntervalHours": {
"placeholders": { "placeholders": {
@@ -1033,13 +1033,13 @@
"repeater_rebootRepeater": "Riavvia Ripetitore", "repeater_rebootRepeater": "Riavvia Ripetitore",
"repeater_rebootRepeaterSubtitle": "Riavvia il dispositivo ripetitore", "repeater_rebootRepeaterSubtitle": "Riavvia il dispositivo ripetitore",
"repeater_rebootRepeaterConfirm": "Sei sicuro di voler riavviare questo ripetitore?", "repeater_rebootRepeaterConfirm": "Sei sicuro di voler riavviare questo ripetitore?",
"repeater_regenerateIdentityKey": "Rigenera Chiave Identità", "repeater_regenerateIdentityKey": "Rigenera Chiave Identità",
"repeater_regenerateIdentityKeySubtitle": "Genera una nuova coppia di chiavi pubblica/privata", "repeater_regenerateIdentityKeySubtitle": "Genera una nuova coppia di chiavi pubblica/privata",
"repeater_regenerateIdentityKeyConfirm": "Questo genererà una nuova identità per il ripetitore. Procedere?", "repeater_regenerateIdentityKeyConfirm": "Questo genererà una nuova identità per il ripetitore. Procedere?",
"repeater_eraseFileSystem": "Elimina File System", "repeater_eraseFileSystem": "Elimina File System",
"repeater_eraseFileSystemSubtitle": "Formatta il file system del ripetitore", "repeater_eraseFileSystemSubtitle": "Formatta il file system del ripetitore",
"repeater_eraseFileSystemConfirm": "ATTENZIONE: Ciò cancellerà tutti i dati sul ripetitore. Non può essere annullato!", "repeater_eraseFileSystemConfirm": "ATTENZIONE: Ciò cancellerà tutti i dati sul ripetitore. Non può essere annullato!",
"repeater_eraseSerialOnly": "Elimina è disponibile solo tramite console seriale.", "repeater_eraseSerialOnly": "Elimina è disponibile solo tramite console seriale.",
"repeater_commandSent": "Comando inviato: {command}", "repeater_commandSent": "Comando inviato: {command}",
"@repeater_commandSent": { "@repeater_commandSent": {
"placeholders": { "placeholders": {
@@ -1072,7 +1072,7 @@
"repeater_refreshLocationSettings": "Aggiorna le Impostazioni della Posizione", "repeater_refreshLocationSettings": "Aggiorna le Impostazioni della Posizione",
"repeater_refreshPacketForwarding": "Aggiorna il inoltro pacchetti", "repeater_refreshPacketForwarding": "Aggiorna il inoltro pacchetti",
"repeater_refreshGuestAccess": "Aggiorna Accesso Ospite", "repeater_refreshGuestAccess": "Aggiorna Accesso Ospite",
"repeater_refreshPrivacyMode": "Aggiorna Modalità Privacy", "repeater_refreshPrivacyMode": "Aggiorna Modalità Privacy",
"repeater_refreshAdvertisementSettings": "Aggiorna le Impostazioni dell'Annuncio", "repeater_refreshAdvertisementSettings": "Aggiorna le Impostazioni dell'Annuncio",
"repeater_refreshed": "{label} aggiornato", "repeater_refreshed": "{label} aggiornato",
"@repeater_refreshed": { "@repeater_refreshed": {
@@ -1117,7 +1117,7 @@
"repeater_cliQuickAdvertise": "Pubblicare", "repeater_cliQuickAdvertise": "Pubblicare",
"repeater_cliQuickClock": "Orologio", "repeater_cliQuickClock": "Orologio",
"repeater_cliHelpAdvert": "Invia un pacchetto pubblicitario", "repeater_cliHelpAdvert": "Invia un pacchetto pubblicitario",
"repeater_cliHelpReboot": "Riavvia il dispositivo. (nota, potresti ottenere 'Timeout' che è normale)", "repeater_cliHelpReboot": "Riavvia il dispositivo. (nota, potresti ottenere 'Timeout' che è normale)",
"repeater_cliHelpClock": "Mostra l'ora corrente per l'orologio di ciascun dispositivo.", "repeater_cliHelpClock": "Mostra l'ora corrente per l'orologio di ciascun dispositivo.",
"repeater_cliHelpPassword": "Imposta una nuova password di amministratore per il dispositivo.", "repeater_cliHelpPassword": "Imposta una nuova password di amministratore per il dispositivo.",
"repeater_cliHelpVersion": "Mostra la versione del dispositivo e la data di costruzione del firmware.", "repeater_cliHelpVersion": "Mostra la versione del dispositivo e la data di costruzione del firmware.",
@@ -1125,12 +1125,12 @@
"repeater_cliHelpSetAf": "Imposta il fattore di tempo di trasmissione.", "repeater_cliHelpSetAf": "Imposta il fattore di tempo di trasmissione.",
"repeater_cliHelpSetTx": "Imposta la potenza di trasmissione LoRa in dBm (riavvia per applicare).", "repeater_cliHelpSetTx": "Imposta la potenza di trasmissione LoRa in dBm (riavvia per applicare).",
"repeater_cliHelpSetRepeat": "Abilita o disabilita il ruolo del ripetitore per questo nodo.", "repeater_cliHelpSetRepeat": "Abilita o disabilita il ruolo del ripetitore per questo nodo.",
"repeater_cliHelpSetAllowReadOnly": "(Server della stanza) Se 'on', allora l'accesso con una password vuota sarà consentito, ma non sarà possibile pubblicare nella stanza. (solo lettura).", "repeater_cliHelpSetAllowReadOnly": "(Server della stanza) Se 'on', allora l'accesso con una password vuota sarà consentito, ma non sarà possibile pubblicare nella stanza. (solo lettura).",
"repeater_cliHelpSetFloodMax": "Imposta il numero massimo di salti per i pacchetti di inondazione in entrata (se >= max, il pacchetto non viene inoltrato)", "repeater_cliHelpSetFloodMax": "Imposta il numero massimo di salti per i pacchetti di inondazione in entrata (se >= max, il pacchetto non viene inoltrato)",
"repeater_cliHelpSetIntThresh": "Imposta il Limite di Interferenza (in dB). Il valore predefinito è 14. Imposta su 0 per disabilitare il rilevamento delle interferenze del canale.", "repeater_cliHelpSetIntThresh": "Imposta il Limite di Interferenza (in dB). Il valore predefinito è 14. Imposta su 0 per disabilitare il rilevamento delle interferenze del canale.",
"repeater_cliHelpSetAgcResetInterval": "Imposta l'intervallo per resettare il controllore Automatico del Guadagno. Imposta su 0 per disabilitare.", "repeater_cliHelpSetAgcResetInterval": "Imposta l'intervallo per resettare il controllore Automatico del Guadagno. Imposta su 0 per disabilitare.",
"repeater_cliHelpSetMultiAcks": "Abilita o disabilita la funzione 'double ACKs'.", "repeater_cliHelpSetMultiAcks": "Abilita o disabilita la funzione 'double ACKs'.",
"repeater_cliHelpSetAdvertInterval": "Imposta l'intervallo del timer in minuti per inviare un pacchetto di pubblicità locale (senza salto). Imposta su 0 per disabilitare.", "repeater_cliHelpSetAdvertInterval": "Imposta l'intervallo del timer in minuti per inviare un pacchetto di pubblicità locale (senza salto). Imposta su 0 per disabilitare.",
"repeater_cliHelpSetFloodAdvertInterval": "Imposta l'intervallo del timer in ore per inviare un pacchetto pubblicitario di massa. Imposta su 0 per disabilitare.", "repeater_cliHelpSetFloodAdvertInterval": "Imposta l'intervallo del timer in ore per inviare un pacchetto pubblicitario di massa. Imposta su 0 per disabilitare.",
"repeater_cliHelpSetGuestPassword": "Imposta/aggiorna la password dell'ospite. (per ripetitori, gli accessi degli ospiti possono inviare la richiesta \"Get Stats\")", "repeater_cliHelpSetGuestPassword": "Imposta/aggiorna la password dell'ospite. (per ripetitori, gli accessi degli ospiti possono inviare la richiesta \"Get Stats\")",
"repeater_cliHelpSetName": "Imposta il nome dell'annuncio.", "repeater_cliHelpSetName": "Imposta il nome dell'annuncio.",
@@ -1138,33 +1138,33 @@
"repeater_cliHelpSetLon": "Imposta la longitudine della mappa pubblicitaria. (gradi decimali)", "repeater_cliHelpSetLon": "Imposta la longitudine della mappa pubblicitaria. (gradi decimali)",
"repeater_cliHelpSetRadio": "Imposta completamente nuovi parametri radio e li salva nelle preferenze. Richiede un comando \"reboot\" per l'applicazione.", "repeater_cliHelpSetRadio": "Imposta completamente nuovi parametri radio e li salva nelle preferenze. Richiede un comando \"reboot\" per l'applicazione.",
"repeater_cliHelpSetRxDelay": "Impostazioni (experimental) base (deve essere > 1 per l'effetto) per applicare un leggero ritardo ai pacchetti ricevuti, in base alla forza del segnale/punteggio. Imposta a 0 per disabilitare.", "repeater_cliHelpSetRxDelay": "Impostazioni (experimental) base (deve essere > 1 per l'effetto) per applicare un leggero ritardo ai pacchetti ricevuti, in base alla forza del segnale/punteggio. Imposta a 0 per disabilitare.",
"repeater_cliHelpSetTxDelay": "Imposta un fattore moltiplicato con il tempo di mantenimento per un pacchetto di modalità allagamento e con un sistema di slot casuale, per ritardarne la trasmissione (per diminuire la probabilità di collisioni).", "repeater_cliHelpSetTxDelay": "Imposta un fattore moltiplicato con il tempo di mantenimento per un pacchetto di modalità allagamento e con un sistema di slot casuale, per ritardarne la trasmissione (per diminuire la probabilità di collisioni).",
"repeater_cliHelpSetDirectTxDelay": "Uguale a txdelay, ma per applicare un ritardo casuale alla inoltrata di pacchetti in modalità diretta.", "repeater_cliHelpSetDirectTxDelay": "Uguale a txdelay, ma per applicare un ritardo casuale alla inoltrata di pacchetti in modalità diretta.",
"repeater_cliHelpSetBridgeEnabled": "Abilita/Disabilita ponte.", "repeater_cliHelpSetBridgeEnabled": "Abilita/Disabilita ponte.",
"repeater_cliHelpSetBridgeDelay": "Imposta il ritardo prima di ritrasmettere i pacchetti.", "repeater_cliHelpSetBridgeDelay": "Imposta il ritardo prima di ritrasmettere i pacchetti.",
"repeater_cliHelpSetBridgeSource": "Scegliere se il ponte dovrà ritrasmettere i pacchetti ricevuti o i pacchetti trasmessi.", "repeater_cliHelpSetBridgeSource": "Scegliere se il ponte dovrà ritrasmettere i pacchetti ricevuti o i pacchetti trasmessi.",
"repeater_cliHelpSetBridgeBaud": "Imposta la velocità di trasmissione per i ponti rs232.", "repeater_cliHelpSetBridgeBaud": "Imposta la velocità di trasmissione per i ponti rs232.",
"repeater_cliHelpSetBridgeSecret": "Imposta il segreto per i ponti espnow.", "repeater_cliHelpSetBridgeSecret": "Imposta il segreto per i ponti espnow.",
"repeater_cliHelpSetAdcMultiplier": "Imposta un fattore personalizzato per regolare la tensione della batteria riportata (supportato solo su schede selezionate).", "repeater_cliHelpSetAdcMultiplier": "Imposta un fattore personalizzato per regolare la tensione della batteria riportata (supportato solo su schede selezionate).",
"repeater_cliHelpTempRadio": "Imposta parametri radio temporanei per il numero specificato di minuti, per poi tornare ai parametri radio originali. (non salva nelle preferenze).", "repeater_cliHelpTempRadio": "Imposta parametri radio temporanei per il numero specificato di minuti, per poi tornare ai parametri radio originali. (non salva nelle preferenze).",
"repeater_cliHelpSetPerm": "Modifica l'ACL. Rimuove l'entrata corrispondente (per prefisso di pubkey) se \"permissions\" è zero. Aggiunge una nuova entrata se il pubkey-hex ha lunghezza completa e non è attualmente nell'ACL. Aggiorna l'entrata per corrispondenza del prefisso di pubkey. I bit di permesso variano per ogni ruolo di firmware, ma i primi 2 bit sono: 0 (Guest), 1 (solo lettura), 2 (lettura/scrittura), 3 (Admin)", "repeater_cliHelpSetPerm": "Modifica l'ACL. Rimuove l'entrata corrispondente (per prefisso di pubkey) se \"permissions\" è zero. Aggiunge una nuova entrata se il pubkey-hex ha lunghezza completa e non è attualmente nell'ACL. Aggiorna l'entrata per corrispondenza del prefisso di pubkey. I bit di permesso variano per ogni ruolo di firmware, ma i primi 2 bit sono: 0 (Guest), 1 (solo lettura), 2 (lettura/scrittura), 3 (Admin)",
"repeater_cliHelpGetBridgeType": "Ottiene tipo ponte nessuno, rs232, espnow", "repeater_cliHelpGetBridgeType": "Ottiene tipo ponte nessuno, rs232, espnow",
"repeater_cliHelpLogStart": "Avvia registrazione pacchetti nel file system.", "repeater_cliHelpLogStart": "Avvia registrazione pacchetti nel file system.",
"repeater_cliHelpLogStop": "Interrompi la registrazione dei pacchetti al file system.", "repeater_cliHelpLogStop": "Interrompi la registrazione dei pacchetti al file system.",
"repeater_cliHelpLogErase": "Elimina i log del pacchetto dal file system.", "repeater_cliHelpLogErase": "Elimina i log del pacchetto dal file system.",
"repeater_cliHelpNeighbors": "Mostra un elenco di altri nodi repeater ricevuti tramite annunci zero-hop. Ogni riga è id-prefisso-esadecimale:timestamp:snr-volte-4", "repeater_cliHelpNeighbors": "Mostra un elenco di altri nodi repeater ricevuti tramite annunci zero-hop. Ogni riga è id-prefisso-esadecimale:timestamp:snr-volte-4",
"repeater_cliHelpNeighborRemove": "Rimuove la prima corrispondenza in base al prefisso (esadecimale) della pubkey, dalla lista dei vicini.", "repeater_cliHelpNeighborRemove": "Rimuove la prima corrispondenza in base al prefisso (esadecimale) della pubkey, dalla lista dei vicini.",
"repeater_cliHelpRegion": "(solo serie) Elenca tutte le regioni definite e le autorizzazioni di allagamento correnti.", "repeater_cliHelpRegion": "(solo serie) Elenca tutte le regioni definite e le autorizzazioni di allagamento correnti.",
"repeater_cliHelpRegionLoad": "NOTA: questo è un'invocazione multi-comando speciale. Ogni comando successivo è un nome di regione (indentato con spazi per indicare la gerarchia parentale, con almeno uno spazio). Terminata inviando una riga vuota/comando.", "repeater_cliHelpRegionLoad": "NOTA: questo è un'invocazione multi-comando speciale. Ogni comando successivo è un nome di regione (indentato con spazi per indicare la gerarchia parentale, con almeno uno spazio). Terminata inviando una riga vuota/comando.",
"repeater_cliHelpRegionGet": "Cerca la regione con il prefisso del nome dato (o \"\" per l'ambito globale). Risponde con \"-> nome-regione (nome-genitore) 'F'\"", "repeater_cliHelpRegionGet": "Cerca la regione con il prefisso del nome dato (o \"\" per l'ambito globale). Risponde con \"-> nome-regione (nome-genitore) 'F'\"",
"repeater_cliHelpRegionPut": "Aggiunge o aggiorna una definizione di regione con il nome specificato.", "repeater_cliHelpRegionPut": "Aggiunge o aggiorna una definizione di regione con il nome specificato.",
"repeater_cliHelpRegionRemove": "Rimuove una definizione di regione con il dato nome. (deve corrispondere esattamente e non avere regioni figlio)", "repeater_cliHelpRegionRemove": "Rimuove una definizione di regione con il dato nome. (deve corrispondere esattamente e non avere regioni figlio)",
"repeater_cliHelpRegionAllowf": "Imposta il permesso di 'F'lood per la regione specificata. ('' per lo scope globale/legacy)", "repeater_cliHelpRegionAllowf": "Imposta il permesso di 'F'lood per la regione specificata. ('' per lo scope globale/legacy)",
"repeater_cliHelpRegionDenyf": "Rimuove il permesso 'F'lood per la regione specificata. (NOTA: a questo stadio non è consigliato utilizzarlo sullo scope globale/legacy!!).", "repeater_cliHelpRegionDenyf": "Rimuove il permesso 'F'lood per la regione specificata. (NOTA: a questo stadio non è consigliato utilizzarlo sullo scope globale/legacy!!).",
"repeater_cliHelpRegionHome": "Risposte con la regione 'home' corrente. (Nota applicata finora, riservata per il futuro)", "repeater_cliHelpRegionHome": "Risposte con la regione 'home' corrente. (Nota applicata finora, riservata per il futuro)",
"repeater_cliHelpRegionHomeSet": "Imposta la regione 'home'.", "repeater_cliHelpRegionHomeSet": "Imposta la regione 'home'.",
"repeater_cliHelpRegionSave": "Persiste l'elenco/mappa delle regioni all'archiviazione.", "repeater_cliHelpRegionSave": "Persiste l'elenco/mappa delle regioni all'archiviazione.",
"repeater_cliHelpGps": "Mostra lo stato del GPS. Quando il GPS è spento, risponde solo \"spento\", se è acceso risponde con \"acceso\", \"stato\", \"fix\" e numero di satelliti.", "repeater_cliHelpGps": "Mostra lo stato del GPS. Quando il GPS è spento, risponde solo \"spento\", se è acceso risponde con \"acceso\", \"stato\", \"fix\" e numero di satelliti.",
"repeater_cliHelpGpsOnOff": "Attiva/disattiva l'alimentazione del GPS.", "repeater_cliHelpGpsOnOff": "Attiva/disattiva l'alimentazione del GPS.",
"repeater_cliHelpGpsSync": "Sincronizza l'orario del nodo con l'orologio GPS.", "repeater_cliHelpGpsSync": "Sincronizza l'orario del nodo con l'orologio GPS.",
"repeater_cliHelpGpsSetLoc": "Imposta la posizione del nodo alle coordinate GPS e salva le preferenze.", "repeater_cliHelpGpsSetLoc": "Imposta la posizione del nodo alle coordinate GPS e salva le preferenze.",
@@ -1180,7 +1180,7 @@
"repeater_regionManagementRepeaterOnly": "Gestione Regione (solo Ripetitore)", "repeater_regionManagementRepeaterOnly": "Gestione Regione (solo Ripetitore)",
"repeater_regionNote": "Sono state introdotte le comandi di regione per gestire le definizioni e le autorizzazioni delle regioni.", "repeater_regionNote": "Sono state introdotte le comandi di regione per gestire le definizioni e le autorizzazioni delle regioni.",
"repeater_gpsManagement": "Gestione GPS", "repeater_gpsManagement": "Gestione GPS",
"repeater_gpsNote": "è stata introdotta una funzione gps per gestire le tematiche relative alla posizione.", "repeater_gpsNote": "è stata introdotta una funzione gps per gestire le tematiche relative alla posizione.",
"telemetry_receivedData": "Dati Telemetria Ricevuti", "telemetry_receivedData": "Dati Telemetria Ricevuti",
"telemetry_requestTimeout": "Richiesta di telemetria scaduta.", "telemetry_requestTimeout": "Richiesta di telemetria scaduta.",
"telemetry_errorLoading": "Errore nel caricamento della telemetria: {error}", "telemetry_errorLoading": "Errore nel caricamento della telemetria: {error}",
@@ -1232,7 +1232,7 @@
} }
} }
}, },
"telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F",
"@telemetry_temperatureValue": { "@telemetry_temperatureValue": {
"placeholders": { "placeholders": {
"celsius": { "celsius": {
@@ -1254,7 +1254,7 @@
"channelPath_repeatsLabel": "Ripeti", "channelPath_repeatsLabel": "Ripeti",
"channelPath_pathLabel": "Percorso {index}", "channelPath_pathLabel": "Percorso {index}",
"channelPath_observedLabel": "Osservato", "channelPath_observedLabel": "Osservato",
"channelPath_observedPathTitle": "Percorso osservato {index} {hops}", "channelPath_observedPathTitle": "Percorso osservato {index} • {hops}",
"@channelPath_observedPathTitle": { "@channelPath_observedPathTitle": {
"placeholders": { "placeholders": {
"index": { "index": {
@@ -1329,7 +1329,7 @@
}, },
"channelPath_pathLabelTitle": "Percorso", "channelPath_pathLabelTitle": "Percorso",
"channelPath_observedPathHeader": "Percorso Osservato", "channelPath_observedPathHeader": "Percorso Osservato",
"channelPath_selectedPathLabel": "{label} {prefixes}", "channelPath_selectedPathLabel": "{label} • {prefixes}",
"@channelPath_selectedPathLabel": { "@channelPath_selectedPathLabel": {
"placeholders": { "placeholders": {
"label": { "label": {
@@ -1373,11 +1373,11 @@
"channels_joinPrivateChannel": "Unisciti a un Canale Privato", "channels_joinPrivateChannel": "Unisciti a un Canale Privato",
"channels_joinPrivateChannelDesc": "Inserire manualmente una chiave segreta.", "channels_joinPrivateChannelDesc": "Inserire manualmente una chiave segreta.",
"channels_joinPublicChannel": "Unisciti al Canale Pubblico", "channels_joinPublicChannel": "Unisciti al Canale Pubblico",
"channels_joinPublicChannelDesc": "Chiunque può unirsi a questo canale.", "channels_joinPublicChannelDesc": "Chiunque può unirsi a questo canale.",
"channels_joinHashtagChannel": "Unisciti a un Canale con Hashtag", "channels_joinHashtagChannel": "Unisciti a un Canale con Hashtag",
"channels_joinHashtagChannelDesc": "Chiunque può unirsi ai canali hashtag.", "channels_joinHashtagChannelDesc": "Chiunque può unirsi ai canali hashtag.",
"channels_scanQrCode": "Scansiona un codice QR", "channels_scanQrCode": "Scansiona un codice QR",
"channels_scanQrCodeComingSoon": "Arriverà presto", "channels_scanQrCodeComingSoon": "Arriverà presto",
"channels_enterHashtag": "Inserisci hashtag", "channels_enterHashtag": "Inserisci hashtag",
"channels_hashtagHint": "es. #team", "channels_hashtagHint": "es. #team",
"@neighbors_unknownContact": { "@neighbors_unknownContact": {
@@ -1459,35 +1459,35 @@
} }
}, },
"common_ok": "OK", "common_ok": "OK",
"community_title": "Comunità", "community_title": "Comunità",
"community_create": "Crea Comunità", "community_create": "Crea Comunità",
"community_createDesc": "Crea una nuova comunità e condividila tramite codice QR.", "community_createDesc": "Crea una nuova comunità e condividila tramite codice QR.",
"community_join": "Unisciti", "community_join": "Unisciti",
"community_joinTitle": "Unisciti alla Community", "community_joinTitle": "Unisciti alla Community",
"community_joinConfirmation": "Vuoi unirti alla community \"{name}\"?", "community_joinConfirmation": "Vuoi unirti alla community \"{name}\"?",
"community_scanQr": "Scansiona il QR Code della Community", "community_scanQr": "Scansiona il QR Code della Community",
"community_scanInstructions": "Punta la fotocamera su un codice QR della comunità", "community_scanInstructions": "Punta la fotocamera su un codice QR della comunità",
"community_showQr": "Mostra il codice QR", "community_showQr": "Mostra il codice QR",
"community_publicChannel": "Comunità Pubblica", "community_publicChannel": "Comunità Pubblica",
"community_hashtagChannel": "Hashtag della Comunità", "community_hashtagChannel": "Hashtag della Comunità",
"community_name": "Nome della Comunità", "community_name": "Nome della Comunità",
"community_enterName": "Inserisci il nome della comunità", "community_enterName": "Inserisci il nome della comunità",
"community_created": "Comunità \"{name}\" creata", "community_created": "Comunità \"{name}\" creata",
"community_joined": "Unito alla comunità \"{name}\"", "community_joined": "Unito alla comunità \"{name}\"",
"community_qrTitle": "Condividi Comunità", "community_qrTitle": "Condividi Comunità",
"community_qrInstructions": "Scansiona questo codice QR per unirti a {name}", "community_qrInstructions": "Scansiona questo codice QR per unirti a {name}",
"community_hashtagPrivacyHint": "I canali hashtag della community sono accessibili solo ai membri della community", "community_hashtagPrivacyHint": "I canali hashtag della community sono accessibili solo ai membri della community",
"community_invalidQrCode": "Codice QR della community non valido", "community_invalidQrCode": "Codice QR della community non valido",
"community_alreadyMember": "Già membro", "community_alreadyMember": "Già membro",
"community_alreadyMemberMessage": "Sei già un membro di \"{name}\".", "community_alreadyMemberMessage": "Sei già un membro di \"{name}\".",
"community_addPublicChannel": "Aggiungi Canale Pubblico della Comunità", "community_addPublicChannel": "Aggiungi Canale Pubblico della Comunità",
"community_addPublicChannelHint": "Aggiungi automaticamente il canale pubblico per questa community", "community_addPublicChannelHint": "Aggiungi automaticamente il canale pubblico per questa community",
"community_noCommunities": "Nessun gruppo aggiunto finora", "community_noCommunities": "Nessun gruppo aggiunto finora",
"community_scanOrCreate": "Scansiona un codice QR o crea una community per iniziare.", "community_scanOrCreate": "Scansiona un codice QR o crea una community per iniziare.",
"community_manageCommunities": "Gestisci Comunità", "community_manageCommunities": "Gestisci Comunità",
"community_delete": "Lascia la Comunità", "community_delete": "Lascia la Comunità",
"community_deleteConfirm": "Uscire da \"{name}\"?", "community_deleteConfirm": "Uscire da \"{name}\"?",
"community_deleteChannelsWarning": "Questo eliminerà anche {count} canale/i e i loro messaggi.", "community_deleteChannelsWarning": "Questo eliminerà anche {count} canale/i e i loro messaggi.",
"@community_deleteChannelsWarning": { "@community_deleteChannelsWarning": {
"placeholders": { "placeholders": {
"count": { "count": {
@@ -1495,14 +1495,14 @@
} }
} }
}, },
"community_deleted": "Hai lasciato la comunità \"{name}\"", "community_deleted": "Hai lasciato la comunità \"{name}\"",
"community_addHashtagChannel": "Aggiungi Hashtag della Community", "community_addHashtagChannel": "Aggiungi Hashtag della Community",
"community_addHashtagChannelDesc": "Aggiungi un canale con hashtag per questa community", "community_addHashtagChannelDesc": "Aggiungi un canale con hashtag per questa community",
"community_selectCommunity": "Seleziona Comunità", "community_selectCommunity": "Seleziona Comunità",
"community_regularHashtag": "Hashtag regolare", "community_regularHashtag": "Hashtag regolare",
"community_regularHashtagDesc": "Hashtag pubblico (chiunque può unirsi)", "community_regularHashtagDesc": "Hashtag pubblico (chiunque può unirsi)",
"community_communityHashtag": "Hashtag della Comunità", "community_communityHashtag": "Hashtag della Comunità",
"community_communityHashtagDesc": "Visibile solo ai membri della comunità", "community_communityHashtagDesc": "Visibile solo ai membri della comunità",
"community_forCommunity": "Per {name}", "community_forCommunity": "Per {name}",
"@community_regenerateSecretConfirm": { "@community_regenerateSecretConfirm": {
"placeholders": { "placeholders": {
@@ -1567,16 +1567,16 @@
"contacts_floodAdvert": "Annuncio alluvionale", "contacts_floodAdvert": "Annuncio alluvionale",
"contacts_copyAdvertToClipboard": "Copia Annuncio negli Appunti", "contacts_copyAdvertToClipboard": "Copia Annuncio negli Appunti",
"contacts_addContactFromClipboard": "Aggiungere contatto dalla clipboard", "contacts_addContactFromClipboard": "Aggiungere contatto dalla clipboard",
"contacts_clipboardEmpty": "La clipboard è vuota.", "contacts_clipboardEmpty": "La clipboard è vuota.",
"contacts_ShareContact": "Copia contatto negli Appunti", "contacts_ShareContact": "Copia contatto negli Appunti",
"contacts_contactImported": "Il contatto è stato importato.", "contacts_contactImported": "Il contatto è stato importato.",
"contacts_contactImportFailed": "Contatto non importato con successo.", "contacts_contactImportFailed": "Contatto non importato con successo.",
"contacts_zeroHopContactAdvertSent": "Inviato contatto tramite annuncio.", "contacts_zeroHopContactAdvertSent": "Inviato contatto tramite annuncio.",
"contacts_contactAdvertCopyFailed": "Copia dell'annuncio nella Clipboard non riuscita.", "contacts_contactAdvertCopyFailed": "Copia dell'annuncio nella Clipboard non riuscita.",
"contacts_ShareContactZeroHop": "Condividi contatto tramite annuncio", "contacts_ShareContactZeroHop": "Condividi contatto tramite annuncio",
"contacts_zeroHopContactAdvertFailed": "Invio del contatto non riuscito.", "contacts_zeroHopContactAdvertFailed": "Invio del contatto non riuscito.",
"contacts_contactAdvertCopied": "Annuncio copiato negli Appunti.", "contacts_contactAdvertCopied": "Annuncio copiato negli Appunti.",
"notification_activityTitle": "Attività MeshCore", "notification_activityTitle": "Attività MeshCore",
"notification_messagesCount": "{count} {count, plural, =1{messaggio} other{messaggi}}", "notification_messagesCount": "{count} {count, plural, =1{messaggio} other{messaggi}}",
"notification_channelMessagesCount": "{count} {count, plural, =1{messaggio del canale} other{messaggi del canale}}", "notification_channelMessagesCount": "{count} {count, plural, =1{messaggio del canale} other{messaggi del canale}}",
"notification_newNodesCount": "{count} {count, plural, =1{nuovo nodo} other{nuovi nodi}}", "notification_newNodesCount": "{count} {count, plural, =1{nuovo nodo} other{nuovi nodi}}",
@@ -1587,7 +1587,7 @@
"settings_gpxExportSuccess": "Esportazione del file GPX completata con successo.", "settings_gpxExportSuccess": "Esportazione del file GPX completata con successo.",
"settings_gpxExportNoContacts": "Nessun contatto da esportare.", "settings_gpxExportNoContacts": "Nessun contatto da esportare.",
"settings_gpxExportNotAvailable": "Non supportato sul tuo dispositivo/Sistema Operativo", "settings_gpxExportNotAvailable": "Non supportato sul tuo dispositivo/Sistema Operativo",
"settings_gpxExportError": "Si è verificato un errore durante l'esportazione.", "settings_gpxExportError": "Si è verificato un errore durante l'esportazione.",
"settings_gpxExportRepeatersSubtitle": "Esporta ripetitori / roomserver con una posizione in un file GPX.", "settings_gpxExportRepeatersSubtitle": "Esporta ripetitori / roomserver con una posizione in un file GPX.",
"settings_gpxExportContactsSubtitle": "Esporta i compagni con una posizione in un file GPX.", "settings_gpxExportContactsSubtitle": "Esporta i compagni con una posizione in un file GPX.",
"settings_gpxExportAll": "Esporta tutti i contatti in GPX", "settings_gpxExportAll": "Esporta tutti i contatti in GPX",
@@ -1597,13 +1597,13 @@
"settings_gpxExportAllContacts": "Tutte le posizioni dei contatti", "settings_gpxExportAllContacts": "Tutte le posizioni dei contatti",
"settings_gpxExportShareText": "Dati mappa esportati da meshcore-open", "settings_gpxExportShareText": "Dati mappa esportati da meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open esportazione dati mappa GPX", "settings_gpxExportShareSubject": "meshcore-open esportazione dati mappa GPX",
"pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!", "pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!",
"map_removeLast": "Rimuovi ultimo", "map_removeLast": "Rimuovi ultimo",
"map_pathTraceCancelled": "Tracciamento del percorso annullato.", "map_pathTraceCancelled": "Tracciamento del percorso annullato.",
"pathTrace_clearTooltip": "Pulisci percorso", "pathTrace_clearTooltip": "Pulisci percorso",
"map_runTrace": "Esegui Path Trace", "map_runTrace": "Esegui Path Trace",
"map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.", "map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.",
"scanner_bluetoothOff": "Il Bluetooth è disattivato.", "scanner_bluetoothOff": "Il Bluetooth è disattivato.",
"scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.", "scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.",
"scanner_chromeRequired": "Browser Chrome richiesto", "scanner_chromeRequired": "Browser Chrome richiesto",
"scanner_chromeRequiredMessage": "Questa applicazione web richiede Google Chrome o un browser basato su Chromium per il supporto Bluetooth.", "scanner_chromeRequiredMessage": "Questa applicazione web richiede Google Chrome o un browser basato su Chromium per il supporto Bluetooth.",
@@ -1612,10 +1612,10 @@
"snrIndicator_lastSeen": "Ultimo accesso", "snrIndicator_lastSeen": "Ultimo accesso",
"chat_ShowAllPaths": "Mostra tutti i percorsi", "chat_ShowAllPaths": "Mostra tutti i percorsi",
"settings_clientRepeat": "Ripetizione \"fuori dalla rete\"", "settings_clientRepeat": "Ripetizione \"fuori dalla rete\"",
"settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.", "settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.",
"settings_clientRepeatSubtitle": "Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri.", "settings_clientRepeatSubtitle": "Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri.",
"settings_aboutOpenMeteoAttribution": "Dati di elevazione LOS: Open-Meteo (CC BY 4.0)", "settings_aboutOpenMeteoAttribution": "Dati di elevazione LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Unità", "appSettings_unitsTitle": "Unità",
"appSettings_unitsMetric": "Metrico (m/km)", "appSettings_unitsMetric": "Metrico (m/km)",
"appSettings_unitsImperial": "Imperiale (ft / mi)", "appSettings_unitsImperial": "Imperiale (ft / mi)",
"map_lineOfSight": "Linea di vista", "map_lineOfSight": "Linea di vista",
@@ -1631,7 +1631,7 @@
}, },
"losClearAllPoints": "Cancella tutti i punti", "losClearAllPoints": "Cancella tutti i punti",
"losRunToViewElevationProfile": "Eseguire LOS per visualizzare il profilo altimetrico", "losRunToViewElevationProfile": "Eseguire LOS per visualizzare il profilo altimetrico",
"losMenuTitle": "Menù LOS", "losMenuTitle": "Menù LOS",
"losMenuSubtitle": "Tocca i nodi o premi a lungo la mappa per punti personalizzati", "losMenuSubtitle": "Tocca i nodi o premi a lungo la mappa per punti personalizzati",
"losShowDisplayNodes": "Mostra i nodi di visualizzazione", "losShowDisplayNodes": "Mostra i nodi di visualizzazione",
"losCustomPoints": "Punti personalizzati", "losCustomPoints": "Punti personalizzati",
@@ -1722,7 +1722,7 @@
} }
} }
}, },
"losErrorElevationUnavailable": "Dati di elevazione non disponibili per uno o più campioni.", "losErrorElevationUnavailable": "Dati di elevazione non disponibili per uno o più campioni.",
"losErrorInvalidInput": "Dati punti/elevazione non validi per il calcolo della LOS.", "losErrorInvalidInput": "Dati punti/elevazione non validi per il calcolo della LOS.",
"losRenameCustomPoint": "Rinomina punto personalizzato", "losRenameCustomPoint": "Rinomina punto personalizzato",
"losPointName": "Nome del punto", "losPointName": "Nome del punto",
@@ -1734,7 +1734,7 @@
"losLegendTerrain": "Terreno", "losLegendTerrain": "Terreno",
"losFrequencyLabel": "Frequenza", "losFrequencyLabel": "Frequenza",
"losFrequencyInfoTooltip": "Visualizza i dettagli del calcolo", "losFrequencyInfoTooltip": "Visualizza i dettagli del calcolo",
"losFrequencyDialogTitle": "Calcolo dellorizzonte radio", "losFrequencyDialogTitle": "Calcolo dell’orizzonte radio",
"losFrequencyDialogDescription": "Partendo da k={baselineK} a {baselineFreq} MHz, il calcolo regola il fattore k per l'attuale banda {frequencyMHz} MHz, che definisce il limite curvo dell'orizzonte radio.", "losFrequencyDialogDescription": "Partendo da k={baselineK} a {baselineFreq} MHz, il calcolo regola il fattore k per l'attuale banda {frequencyMHz} MHz, che definisce il limite curvo dell'orizzonte radio.",
"@losFrequencyDialogDescription": { "@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
@@ -1802,11 +1802,9 @@
"contacts_unread": "Non letti", "contacts_unread": "Non letti",
"contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...", "contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...",
"contacts_searchRoomServers": "Cerca {number}{str} server Room...", "contacts_searchRoomServers": "Cerca {number}{str} server Room...",
"connectionChoiceTitle": "Scegli il metodo di connessione che preferisci.",
"connectionChoiceSubtitle": "Seleziona il metodo che preferisci per accedere al tuo dispositivo MeshCore.",
"connectionChoiceBluetoothLabel": "Bluetooth", "connectionChoiceBluetoothLabel": "Bluetooth",
"connectionChoiceUsbLabel": "USB", "connectionChoiceUsbLabel": "USB",
"usbScreenNote": "La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.", "usbScreenNote": "La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.",
"usbScreenStatus": "Seleziona un dispositivo USB", "usbScreenStatus": "Seleziona un dispositivo USB",
"usbScreenSubtitle": "Seleziona il dispositivo seriale rilevato e connettilo direttamente al tuo nodo MeshCore.", "usbScreenSubtitle": "Seleziona il dispositivo seriale rilevato e connettilo direttamente al tuo nodo MeshCore.",
"usbScreenTitle": "Connessione tramite USB", "usbScreenTitle": "Connessione tramite USB",
+13 -25
View File
@@ -295,7 +295,7 @@ abstract class AppLocalizations {
/// No description provided for @common_notAvailable. /// No description provided for @common_notAvailable.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **''** /// **'—'**
String get common_notAvailable; String get common_notAvailable;
/// No description provided for @common_voltageValue. /// No description provided for @common_voltageValue.
@@ -316,18 +316,6 @@ abstract class AppLocalizations {
/// **'MeshCore Open'** /// **'MeshCore Open'**
String get scanner_title; String get scanner_title;
/// No description provided for @connectionChoiceTitle.
///
/// In en, this message translates to:
/// **'Choose your connection method'**
String get connectionChoiceTitle;
/// No description provided for @connectionChoiceSubtitle.
///
/// In en, this message translates to:
/// **'Select how you would like to reach your MeshCore device.'**
String get connectionChoiceSubtitle;
/// No description provided for @connectionChoiceUsbLabel. /// No description provided for @connectionChoiceUsbLabel.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -955,13 +943,13 @@ abstract class AppLocalizations {
/// No description provided for @appSettings_languageFr. /// No description provided for @appSettings_languageFr.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Français'** /// **'Français'**
String get appSettings_languageFr; String get appSettings_languageFr;
/// No description provided for @appSettings_languageEs. /// No description provided for @appSettings_languageEs.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Español'** /// **'Español'**
String get appSettings_languageEs; String get appSettings_languageEs;
/// No description provided for @appSettings_languageDe. /// No description provided for @appSettings_languageDe.
@@ -979,13 +967,13 @@ abstract class AppLocalizations {
/// No description provided for @appSettings_languageSl. /// No description provided for @appSettings_languageSl.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Slovenščina'** /// **'Slovenščina'**
String get appSettings_languageSl; String get appSettings_languageSl;
/// No description provided for @appSettings_languagePt. /// No description provided for @appSettings_languagePt.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Português'** /// **'Português'**
String get appSettings_languagePt; String get appSettings_languagePt;
/// No description provided for @appSettings_languageIt. /// No description provided for @appSettings_languageIt.
@@ -997,7 +985,7 @@ abstract class AppLocalizations {
/// No description provided for @appSettings_languageZh. /// No description provided for @appSettings_languageZh.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'中文'** /// **'中文'**
String get appSettings_languageZh; String get appSettings_languageZh;
/// No description provided for @appSettings_languageSv. /// No description provided for @appSettings_languageSv.
@@ -1015,25 +1003,25 @@ abstract class AppLocalizations {
/// No description provided for @appSettings_languageSk. /// No description provided for @appSettings_languageSk.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Slovenčina'** /// **'Slovenčina'**
String get appSettings_languageSk; String get appSettings_languageSk;
/// No description provided for @appSettings_languageBg. /// No description provided for @appSettings_languageBg.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Български'** /// **'Български'**
String get appSettings_languageBg; String get appSettings_languageBg;
/// No description provided for @appSettings_languageRu. /// No description provided for @appSettings_languageRu.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Русский'** /// **'Русский'**
String get appSettings_languageRu; String get appSettings_languageRu;
/// No description provided for @appSettings_languageUk. /// No description provided for @appSettings_languageUk.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Українська'** /// **'Українська'**
String get appSettings_languageUk; String get appSettings_languageUk;
/// No description provided for @appSettings_enableMessageTracing. /// No description provided for @appSettings_enableMessageTracing.
@@ -4349,7 +4337,7 @@ abstract class AppLocalizations {
/// No description provided for @telemetry_temperatureValue. /// No description provided for @telemetry_temperatureValue.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'{celsius}°C / {fahrenheit}°F'** /// **'{celsius}°C / {fahrenheit}°F'**
String telemetry_temperatureValue(String celsius, String fahrenheit); String telemetry_temperatureValue(String celsius, String fahrenheit);
/// No description provided for @neighbors_receivedData. /// No description provided for @neighbors_receivedData.
@@ -4463,7 +4451,7 @@ abstract class AppLocalizations {
/// No description provided for @channelPath_observedPathTitle. /// No description provided for @channelPath_observedPathTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Observed path {index} {hops}'** /// **'Observed path {index} • {hops}'**
String channelPath_observedPathTitle(int index, String hops); String channelPath_observedPathTitle(int index, String hops);
/// No description provided for @channelPath_noLocationData. /// No description provided for @channelPath_noLocationData.
@@ -4547,7 +4535,7 @@ abstract class AppLocalizations {
/// No description provided for @channelPath_selectedPathLabel. /// No description provided for @channelPath_selectedPathLabel.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'{label} {prefixes}'** /// **'{label} • {prefixes}'**
String channelPath_selectedPathLabel(String label, String prefixes); String channelPath_selectedPathLabel(String label, String prefixes);
/// No description provided for @channelPath_noHopDetailsAvailable. /// No description provided for @channelPath_noHopDetailsAvailable.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+13 -20
View File
@@ -93,7 +93,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get common_loading => 'Loading...'; String get common_loading => 'Loading...';
@override @override
String get common_notAvailable => ''; String get common_notAvailable => '—';
@override @override
String common_voltageValue(String volts) { String common_voltageValue(String volts) {
@@ -108,13 +108,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get scanner_title => 'MeshCore Open'; String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceTitle => 'Choose your connection method';
@override
String get connectionChoiceSubtitle =>
'Select how you would like to reach your MeshCore device.';
@override @override
String get connectionChoiceUsbLabel => 'USB'; String get connectionChoiceUsbLabel => 'USB';
@@ -455,10 +448,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get appSettings_languageEn => 'English'; String get appSettings_languageEn => 'English';
@override @override
String get appSettings_languageFr => 'Français'; String get appSettings_languageFr => 'Français';
@override @override
String get appSettings_languageEs => 'Español'; String get appSettings_languageEs => 'Español';
@override @override
String get appSettings_languageDe => 'Deutsch'; String get appSettings_languageDe => 'Deutsch';
@@ -467,16 +460,16 @@ class AppLocalizationsEn extends AppLocalizations {
String get appSettings_languagePl => 'Polski'; String get appSettings_languagePl => 'Polski';
@override @override
String get appSettings_languageSl => 'Slovenščina'; String get appSettings_languageSl => 'Slovenščina';
@override @override
String get appSettings_languagePt => 'Português'; String get appSettings_languagePt => 'Português';
@override @override
String get appSettings_languageIt => 'Italiano'; String get appSettings_languageIt => 'Italiano';
@override @override
String get appSettings_languageZh => '中文'; String get appSettings_languageZh => '中文';
@override @override
String get appSettings_languageSv => 'Svenska'; String get appSettings_languageSv => 'Svenska';
@@ -485,16 +478,16 @@ class AppLocalizationsEn extends AppLocalizations {
String get appSettings_languageNl => 'Nederlands'; String get appSettings_languageNl => 'Nederlands';
@override @override
String get appSettings_languageSk => 'Slovenčina'; String get appSettings_languageSk => 'Slovenčina';
@override @override
String get appSettings_languageBg => 'Български'; String get appSettings_languageBg => 'Български';
@override @override
String get appSettings_languageRu => 'Русский'; String get appSettings_languageRu => 'Русский';
@override @override
String get appSettings_languageUk => 'Українська'; String get appSettings_languageUk => 'Українська';
@override @override
String get appSettings_enableMessageTracing => 'Enable Message Tracing'; String get appSettings_enableMessageTracing => 'Enable Message Tracing';
@@ -2431,7 +2424,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String telemetry_temperatureValue(String celsius, String fahrenheit) { String telemetry_temperatureValue(String celsius, String fahrenheit) {
return '$celsius°C / $fahrenheit°F'; return '$celsius°C / $fahrenheit°F';
} }
@override @override
@@ -2499,7 +2492,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String channelPath_observedPathTitle(int index, String hops) { String channelPath_observedPathTitle(int index, String hops) {
return 'Observed path $index $hops'; return 'Observed path $index • $hops';
} }
@override @override
@@ -2554,7 +2547,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String channelPath_selectedPathLabel(String label, String prefixes) { String channelPath_selectedPathLabel(String label, String prefixes) {
return '$label $prefixes'; return '$label • $prefixes';
} }
@override @override
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+96 -103
View File
@@ -93,7 +93,7 @@ class AppLocalizationsIt extends AppLocalizations {
String get common_loading => 'Caricamento...'; String get common_loading => 'Caricamento...';
@override @override
String get common_notAvailable => ''; String get common_notAvailable => '—';
@override @override
String common_voltageValue(String volts) { String common_voltageValue(String volts) {
@@ -108,14 +108,6 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get scanner_title => 'MeshCore Open'; String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceTitle =>
'Scegli il metodo di connessione che preferisci.';
@override
String get connectionChoiceSubtitle =>
'Seleziona il metodo che preferisci per accedere al tuo dispositivo MeshCore.';
@override @override
String get connectionChoiceUsbLabel => 'USB'; String get connectionChoiceUsbLabel => 'USB';
@@ -134,7 +126,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get usbScreenNote => String get usbScreenNote =>
'La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.'; 'La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.';
@override @override
String get usbScreenEmptyState => String get usbScreenEmptyState =>
@@ -176,7 +168,7 @@ class AppLocalizationsIt extends AppLocalizations {
String get scanner_scan => 'Scansiona'; String get scanner_scan => 'Scansiona';
@override @override
String get scanner_bluetoothOff => 'Il Bluetooth è disattivato.'; String get scanner_bluetoothOff => 'Il Bluetooth è disattivato.';
@override @override
String get scanner_bluetoothOffMessage => String get scanner_bluetoothOffMessage =>
@@ -273,7 +265,7 @@ class AppLocalizationsIt extends AppLocalizations {
String get settings_longitude => 'Longitudine'; String get settings_longitude => 'Longitudine';
@override @override
String get settings_privacyMode => 'Modalità Privacy'; String get settings_privacyMode => 'Modalità Privacy';
@override @override
String get settings_privacyModeSubtitle => String get settings_privacyModeSubtitle =>
@@ -281,13 +273,13 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get settings_privacyModeToggle => String get settings_privacyModeToggle =>
'Attiva la modalità privacy per nascondere il tuo nome e la tua posizione negli annunci.'; 'Attiva la modalità privacy per nascondere il tuo nome e la tua posizione negli annunci.';
@override @override
String get settings_privacyModeEnabled => 'Modalità privacy abilitata'; String get settings_privacyModeEnabled => 'Modalità privacy abilitata';
@override @override
String get settings_privacyModeDisabled => 'Modalità privacy disabilitata'; String get settings_privacyModeDisabled => 'Modalità privacy disabilitata';
@override @override
String get settings_actions => 'Azioni'; String get settings_actions => 'Azioni';
@@ -425,7 +417,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get settings_clientRepeatFreqWarning => String get settings_clientRepeatFreqWarning =>
'Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.'; 'Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.';
@override @override
String settings_error(String message) { String settings_error(String message) {
@@ -460,10 +452,10 @@ class AppLocalizationsIt extends AppLocalizations {
String get appSettings_languageEn => 'English'; String get appSettings_languageEn => 'English';
@override @override
String get appSettings_languageFr => 'Français'; String get appSettings_languageFr => 'Français';
@override @override
String get appSettings_languageEs => 'Español'; String get appSettings_languageEs => 'Español';
@override @override
String get appSettings_languageDe => 'Deutsch'; String get appSettings_languageDe => 'Deutsch';
@@ -472,16 +464,16 @@ class AppLocalizationsIt extends AppLocalizations {
String get appSettings_languagePl => 'Polski'; String get appSettings_languagePl => 'Polski';
@override @override
String get appSettings_languageSl => 'Slovenščina'; String get appSettings_languageSl => 'Slovenščina';
@override @override
String get appSettings_languagePt => 'Português'; String get appSettings_languagePt => 'Português';
@override @override
String get appSettings_languageIt => 'Italiano'; String get appSettings_languageIt => 'Italiano';
@override @override
String get appSettings_languageZh => '中文'; String get appSettings_languageZh => '中文';
@override @override
String get appSettings_languageSv => 'Svenska'; String get appSettings_languageSv => 'Svenska';
@@ -490,10 +482,10 @@ class AppLocalizationsIt extends AppLocalizations {
String get appSettings_languageNl => 'Nederlands'; String get appSettings_languageNl => 'Nederlands';
@override @override
String get appSettings_languageSk => 'Slovenčina'; String get appSettings_languageSk => 'Slovenčina';
@override @override
String get appSettings_languageBg => 'Български'; String get appSettings_languageBg => 'Български';
@override @override
String get appSettings_languageRu => 'Russo'; String get appSettings_languageRu => 'Russo';
@@ -576,7 +568,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get appSettings_autoRouteRotationSubtitle => String get appSettings_autoRouteRotationSubtitle =>
'Alterna tra i percorsi migliori e la modalità alluvione'; 'Alterna tra i percorsi migliori e la modalità alluvione';
@override @override
String get appSettings_autoRouteRotationEnabled => String get appSettings_autoRouteRotationEnabled =>
@@ -671,7 +663,7 @@ class AppLocalizationsIt extends AppLocalizations {
String get appSettings_offlineMapCache => 'Cache Mappa Offline'; String get appSettings_offlineMapCache => 'Cache Mappa Offline';
@override @override
String get appSettings_unitsTitle => 'Unità'; String get appSettings_unitsTitle => 'Unità';
@override @override
String get appSettings_unitsMetric => 'Metrico (m/km)'; String get appSettings_unitsMetric => 'Metrico (m/km)';
@@ -790,11 +782,12 @@ class AppLocalizationsIt extends AppLocalizations {
String get contacts_groupName => 'Nome gruppo'; String get contacts_groupName => 'Nome gruppo';
@override @override
String get contacts_groupNameRequired => 'Il nome del gruppo è obbligatorio.'; String get contacts_groupNameRequired =>
'Il nome del gruppo è obbligatorio.';
@override @override
String contacts_groupAlreadyExists(String name) { String contacts_groupAlreadyExists(String name) {
return 'Il gruppo \"$name\" esiste già.'; return 'Il gruppo \"$name\" esiste già.';
} }
@override @override
@@ -880,7 +873,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String channels_deleteChannelConfirm(String name) { String channels_deleteChannelConfirm(String name) {
return 'Eliminare \"$name\"? Non può essere annullato.'; return 'Eliminare \"$name\"? Non può essere annullato.';
} }
@override @override
@@ -977,20 +970,20 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get channels_joinPublicChannelDesc => String get channels_joinPublicChannelDesc =>
'Chiunque può unirsi a questo canale.'; 'Chiunque può unirsi a questo canale.';
@override @override
String get channels_joinHashtagChannel => 'Unisciti a un Canale con Hashtag'; String get channels_joinHashtagChannel => 'Unisciti a un Canale con Hashtag';
@override @override
String get channels_joinHashtagChannelDesc => String get channels_joinHashtagChannelDesc =>
'Chiunque può unirsi ai canali hashtag.'; 'Chiunque può unirsi ai canali hashtag.';
@override @override
String get channels_scanQrCode => 'Scansiona un codice QR'; String get channels_scanQrCode => 'Scansiona un codice QR';
@override @override
String get channels_scanQrCodeComingSoon => 'Arriverà presto'; String get channels_scanQrCodeComingSoon => 'Arriverà presto';
@override @override
String get channels_enterHashtag => 'Inserisci hashtag'; String get channels_enterHashtag => 'Inserisci hashtag';
@@ -1124,7 +1117,7 @@ class AppLocalizationsIt extends AppLocalizations {
String get debugLog_rawLogRx => 'Log Raw-RX'; String get debugLog_rawLogRx => 'Log Raw-RX';
@override @override
String get debugLog_noBleActivity => 'Nessuna attività BLE rilevata ancora.'; String get debugLog_noBleActivity => 'Nessuna attività BLE rilevata ancora.';
@override @override
String debugFrame_length(int count) { String debugFrame_length(int count) {
@@ -1180,20 +1173,20 @@ class AppLocalizationsIt extends AppLocalizations {
String get chat_ShowAllPaths => 'Mostra tutti i percorsi'; String get chat_ShowAllPaths => 'Mostra tutti i percorsi';
@override @override
String get chat_routingMode => 'Modalità di routing'; String get chat_routingMode => 'Modalità di routing';
@override @override
String get chat_autoUseSavedPath => 'Utilizza il percorso salvato'; String get chat_autoUseSavedPath => 'Utilizza il percorso salvato';
@override @override
String get chat_forceFloodMode => 'Modalità Inondamento Forzato'; String get chat_forceFloodMode => 'Modalità Inondamento Forzato';
@override @override
String get chat_recentAckPaths => 'Percorsi ACK Recenti (tocca per usare):'; String get chat_recentAckPaths => 'Percorsi ACK Recenti (tocca per usare):';
@override @override
String get chat_pathHistoryFull => String get chat_pathHistoryFull =>
'La cronologia del percorso è piena. Rimuovi gli elementi per aggiungere nuovi.'; 'La cronologia del percorso è piena. Rimuovi gli elementi per aggiungere nuovi.';
@override @override
String get chat_hopSingular => 'salta'; String get chat_hopSingular => 'salta';
@@ -1220,7 +1213,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get chat_noPathHistoryYet => String get chat_noPathHistoryYet =>
'Non c\'è ancora una cronologia del percorso.\nInvia un messaggio per scoprire i percorsi.'; 'Non c\'è ancora una cronologia del percorso.\nInvia un messaggio per scoprire i percorsi.';
@override @override
String get chat_pathActions => 'Azioni Percorso:'; String get chat_pathActions => 'Azioni Percorso:';
@@ -1241,7 +1234,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get chat_pathCleared => String get chat_pathCleared =>
'Percorso sgomberato. Il prossimo messaggio riidentifierà il percorso.'; 'Percorso sgomberato. Il prossimo messaggio riidentifierà il percorso.';
@override @override
String get chat_floodModeSubtitle => String get chat_floodModeSubtitle =>
@@ -1249,7 +1242,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get chat_floodModeEnabled => String get chat_floodModeEnabled =>
'Modalità alluvione abilitata. Disattivala tramite l\'icona di routing nella barra in alto.'; 'Modalità alluvione abilitata. Disattivala tramite l\'icona di routing nella barra in alto.';
@override @override
String get chat_fullPath => 'Percorso Completo'; String get chat_fullPath => 'Percorso Completo';
@@ -1424,7 +1417,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String map_publicLocationShareConfirm(String channelLabel) { String map_publicLocationShareConfirm(String channelLabel) {
return 'Stai per condividere una posizione in $channelLabel. Questo canale è pubblico e chiunque abbia la PSK può vederlo.'; return 'Stai per condividere una posizione in $channelLabel. Questo canale è pubblico e chiunque abbia la PSK può vederlo.';
} }
@override @override
@@ -1642,7 +1635,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get login_savePasswordSubtitle => String get login_savePasswordSubtitle =>
'La password verrà memorizzata in modo sicuro su questo dispositivo.'; 'La password verrà memorizzata in modo sicuro su questo dispositivo.';
@override @override
String get login_repeaterDescription => String get login_repeaterDescription =>
@@ -1656,13 +1649,13 @@ class AppLocalizationsIt extends AppLocalizations {
String get login_routing => 'Instradamento'; String get login_routing => 'Instradamento';
@override @override
String get login_routingMode => 'Modalità di routing'; String get login_routingMode => 'Modalità di routing';
@override @override
String get login_autoUseSavedPath => 'Utilizza il percorso salvato'; String get login_autoUseSavedPath => 'Utilizza il percorso salvato';
@override @override
String get login_forceFloodMode => 'Modalità Inondamento Forzato'; String get login_forceFloodMode => 'Modalità Inondamento Forzato';
@override @override
String get login_managePaths => 'Gestisci Percorsi'; String get login_managePaths => 'Gestisci Percorsi';
@@ -1682,7 +1675,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get login_failedMessage => String get login_failedMessage =>
'Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.'; 'Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.';
@override @override
String get common_reload => 'Ricaricare'; String get common_reload => 'Ricaricare';
@@ -1725,7 +1718,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get path_helperMaxHops => String get path_helperMaxHops =>
'Massimo 64 salti. Ogni prefisso è composto da 2 caratteri esadecimali (1 byte)'; 'Massimo 64 salti. Ogni prefisso è composto da 2 caratteri esadecimali (1 byte)';
@override @override
String get path_selectFromContacts => 'Seleziona da contatti:'; String get path_selectFromContacts => 'Seleziona da contatti:';
@@ -1745,7 +1738,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get path_tooLong => String get path_tooLong =>
'Il percorso è troppo lungo. Massimo 64 salti consentiti.'; 'Il percorso è troppo lungo. Massimo 64 salti consentiti.';
@override @override
String get path_setPath => 'Imposta Percorso'; String get path_setPath => 'Imposta Percorso';
@@ -1797,13 +1790,13 @@ class AppLocalizationsIt extends AppLocalizations {
String get repeater_statusTitle => 'Stato del Ripetitore'; String get repeater_statusTitle => 'Stato del Ripetitore';
@override @override
String get repeater_routingMode => 'Modalità di routing'; String get repeater_routingMode => 'Modalità di routing';
@override @override
String get repeater_autoUseSavedPath => 'Percorso salvato automatico'; String get repeater_autoUseSavedPath => 'Percorso salvato automatico';
@override @override
String get repeater_forceFloodMode => 'Modalità Inondamento Forzato'; String get repeater_forceFloodMode => 'Modalità Inondamento Forzato';
@override @override
String get repeater_pathManagement => 'Gestione dei percorsi'; String get repeater_pathManagement => 'Gestione dei percorsi';
@@ -1829,7 +1822,7 @@ class AppLocalizationsIt extends AppLocalizations {
String get repeater_clockAtLogin => 'Orologio (all\'accesso)'; String get repeater_clockAtLogin => 'Orologio (all\'accesso)';
@override @override
String get repeater_uptime => 'Disponibilità'; String get repeater_uptime => 'Disponibilità';
@override @override
String get repeater_queueLength => 'Lunghezza della coda'; String get repeater_queueLength => 'Lunghezza della coda';
@@ -1981,7 +1974,7 @@ class AppLocalizationsIt extends AppLocalizations {
'Consenti l\'accesso ospite in sola lettura'; 'Consenti l\'accesso ospite in sola lettura';
@override @override
String get repeater_privacyMode => 'Modalità Privacy'; String get repeater_privacyMode => 'Modalità Privacy';
@override @override
String get repeater_privacyModeSubtitle => String get repeater_privacyModeSubtitle =>
@@ -1991,7 +1984,7 @@ class AppLocalizationsIt extends AppLocalizations {
String get repeater_advertisementSettings => 'Impostazioni Annuncio'; String get repeater_advertisementSettings => 'Impostazioni Annuncio';
@override @override
String get repeater_localAdvertInterval => 'Intervallo Pubblicità Locale'; String get repeater_localAdvertInterval => 'Intervallo Pubblicità Locale';
@override @override
String repeater_localAdvertIntervalMinutes(int minutes) { String repeater_localAdvertIntervalMinutes(int minutes) {
@@ -2000,7 +1993,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get repeater_floodAdvertInterval => String get repeater_floodAdvertInterval =>
'Intervallo Pubblicità Inondazione'; 'Intervallo Pubblicità Inondazione';
@override @override
String repeater_floodAdvertIntervalHours(int hours) { String repeater_floodAdvertIntervalHours(int hours) {
@@ -2026,7 +2019,7 @@ class AppLocalizationsIt extends AppLocalizations {
'Sei sicuro di voler riavviare questo ripetitore?'; 'Sei sicuro di voler riavviare questo ripetitore?';
@override @override
String get repeater_regenerateIdentityKey => 'Rigenera Chiave Identità'; String get repeater_regenerateIdentityKey => 'Rigenera Chiave Identità';
@override @override
String get repeater_regenerateIdentityKeySubtitle => String get repeater_regenerateIdentityKeySubtitle =>
@@ -2034,7 +2027,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get repeater_regenerateIdentityKeyConfirm => String get repeater_regenerateIdentityKeyConfirm =>
'Questo genererà una nuova identità per il ripetitore. Procedere?'; 'Questo genererà una nuova identità per il ripetitore. Procedere?';
@override @override
String get repeater_eraseFileSystem => 'Elimina File System'; String get repeater_eraseFileSystem => 'Elimina File System';
@@ -2045,11 +2038,11 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get repeater_eraseFileSystemConfirm => String get repeater_eraseFileSystemConfirm =>
'ATTENZIONE: Ciò cancellerà tutti i dati sul ripetitore. Non può essere annullato!'; 'ATTENZIONE: Ciò cancellerà tutti i dati sul ripetitore. Non può essere annullato!';
@override @override
String get repeater_eraseSerialOnly => String get repeater_eraseSerialOnly =>
'Elimina è disponibile solo tramite console seriale.'; 'Elimina è disponibile solo tramite console seriale.';
@override @override
String repeater_commandSent(String command) { String repeater_commandSent(String command) {
@@ -2093,7 +2086,7 @@ class AppLocalizationsIt extends AppLocalizations {
String get repeater_refreshGuestAccess => 'Aggiorna Accesso Ospite'; String get repeater_refreshGuestAccess => 'Aggiorna Accesso Ospite';
@override @override
String get repeater_refreshPrivacyMode => 'Aggiorna Modalità Privacy'; String get repeater_refreshPrivacyMode => 'Aggiorna Modalità Privacy';
@override @override
String get repeater_refreshAdvertisementSettings => String get repeater_refreshAdvertisementSettings =>
@@ -2174,7 +2167,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get repeater_cliHelpReboot => String get repeater_cliHelpReboot =>
'Riavvia il dispositivo. (nota, potresti ottenere \'Timeout\' che è normale)'; 'Riavvia il dispositivo. (nota, potresti ottenere \'Timeout\' che è normale)';
@override @override
String get repeater_cliHelpClock => String get repeater_cliHelpClock =>
@@ -2206,7 +2199,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get repeater_cliHelpSetAllowReadOnly => String get repeater_cliHelpSetAllowReadOnly =>
'(Server della stanza) Se \'on\', allora l\'accesso con una password vuota sarà consentito, ma non sarà possibile pubblicare nella stanza. (solo lettura).'; '(Server della stanza) Se \'on\', allora l\'accesso con una password vuota sarà consentito, ma non sarà possibile pubblicare nella stanza. (solo lettura).';
@override @override
String get repeater_cliHelpSetFloodMax => String get repeater_cliHelpSetFloodMax =>
@@ -2214,7 +2207,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get repeater_cliHelpSetIntThresh => String get repeater_cliHelpSetIntThresh =>
'Imposta il Limite di Interferenza (in dB). Il valore predefinito è 14. Imposta su 0 per disabilitare il rilevamento delle interferenze del canale.'; 'Imposta il Limite di Interferenza (in dB). Il valore predefinito è 14. Imposta su 0 per disabilitare il rilevamento delle interferenze del canale.';
@override @override
String get repeater_cliHelpSetAgcResetInterval => String get repeater_cliHelpSetAgcResetInterval =>
@@ -2226,7 +2219,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get repeater_cliHelpSetAdvertInterval => String get repeater_cliHelpSetAdvertInterval =>
'Imposta l\'intervallo del timer in minuti per inviare un pacchetto di pubblicità locale (senza salto). Imposta su 0 per disabilitare.'; 'Imposta l\'intervallo del timer in minuti per inviare un pacchetto di pubblicità locale (senza salto). Imposta su 0 per disabilitare.';
@override @override
String get repeater_cliHelpSetFloodAdvertInterval => String get repeater_cliHelpSetFloodAdvertInterval =>
@@ -2257,11 +2250,11 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get repeater_cliHelpSetTxDelay => String get repeater_cliHelpSetTxDelay =>
'Imposta un fattore moltiplicato con il tempo di mantenimento per un pacchetto di modalità allagamento e con un sistema di slot casuale, per ritardarne la trasmissione (per diminuire la probabilità di collisioni).'; 'Imposta un fattore moltiplicato con il tempo di mantenimento per un pacchetto di modalità allagamento e con un sistema di slot casuale, per ritardarne la trasmissione (per diminuire la probabilità di collisioni).';
@override @override
String get repeater_cliHelpSetDirectTxDelay => String get repeater_cliHelpSetDirectTxDelay =>
'Uguale a txdelay, ma per applicare un ritardo casuale alla inoltrata di pacchetti in modalità diretta.'; 'Uguale a txdelay, ma per applicare un ritardo casuale alla inoltrata di pacchetti in modalità diretta.';
@override @override
String get repeater_cliHelpSetBridgeEnabled => 'Abilita/Disabilita ponte.'; String get repeater_cliHelpSetBridgeEnabled => 'Abilita/Disabilita ponte.';
@@ -2272,11 +2265,11 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get repeater_cliHelpSetBridgeSource => String get repeater_cliHelpSetBridgeSource =>
'Scegliere se il ponte dovrà ritrasmettere i pacchetti ricevuti o i pacchetti trasmessi.'; 'Scegliere se il ponte dovrà ritrasmettere i pacchetti ricevuti o i pacchetti trasmessi.';
@override @override
String get repeater_cliHelpSetBridgeBaud => String get repeater_cliHelpSetBridgeBaud =>
'Imposta la velocità di trasmissione per i ponti rs232.'; 'Imposta la velocità di trasmissione per i ponti rs232.';
@override @override
String get repeater_cliHelpSetBridgeSecret => String get repeater_cliHelpSetBridgeSecret =>
@@ -2292,7 +2285,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get repeater_cliHelpSetPerm => String get repeater_cliHelpSetPerm =>
'Modifica l\'ACL. Rimuove l\'entrata corrispondente (per prefisso di pubkey) se \"permissions\" è zero. Aggiunge una nuova entrata se il pubkey-hex ha lunghezza completa e non è attualmente nell\'ACL. Aggiorna l\'entrata per corrispondenza del prefisso di pubkey. I bit di permesso variano per ogni ruolo di firmware, ma i primi 2 bit sono: 0 (Guest), 1 (solo lettura), 2 (lettura/scrittura), 3 (Admin)'; 'Modifica l\'ACL. Rimuove l\'entrata corrispondente (per prefisso di pubkey) se \"permissions\" è zero. Aggiunge una nuova entrata se il pubkey-hex ha lunghezza completa e non è attualmente nell\'ACL. Aggiorna l\'entrata per corrispondenza del prefisso di pubkey. I bit di permesso variano per ogni ruolo di firmware, ma i primi 2 bit sono: 0 (Guest), 1 (solo lettura), 2 (lettura/scrittura), 3 (Admin)';
@override @override
String get repeater_cliHelpGetBridgeType => String get repeater_cliHelpGetBridgeType =>
@@ -2312,7 +2305,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get repeater_cliHelpNeighbors => String get repeater_cliHelpNeighbors =>
'Mostra un elenco di altri nodi repeater ricevuti tramite annunci zero-hop. Ogni riga è id-prefisso-esadecimale:timestamp:snr-volte-4'; 'Mostra un elenco di altri nodi repeater ricevuti tramite annunci zero-hop. Ogni riga è id-prefisso-esadecimale:timestamp:snr-volte-4';
@override @override
String get repeater_cliHelpNeighborRemove => String get repeater_cliHelpNeighborRemove =>
@@ -2324,7 +2317,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get repeater_cliHelpRegionLoad => String get repeater_cliHelpRegionLoad =>
'NOTA: questo è un\'invocazione multi-comando speciale. Ogni comando successivo è un nome di regione (indentato con spazi per indicare la gerarchia parentale, con almeno uno spazio). Terminata inviando una riga vuota/comando.'; 'NOTA: questo è un\'invocazione multi-comando speciale. Ogni comando successivo è un nome di regione (indentato con spazi per indicare la gerarchia parentale, con almeno uno spazio). Terminata inviando una riga vuota/comando.';
@override @override
String get repeater_cliHelpRegionGet => String get repeater_cliHelpRegionGet =>
@@ -2344,7 +2337,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get repeater_cliHelpRegionDenyf => String get repeater_cliHelpRegionDenyf =>
'Rimuove il permesso \'F\'lood per la regione specificata. (NOTA: a questo stadio non è consigliato utilizzarlo sullo scope globale/legacy!!).'; 'Rimuove il permesso \'F\'lood per la regione specificata. (NOTA: a questo stadio non è consigliato utilizzarlo sullo scope globale/legacy!!).';
@override @override
String get repeater_cliHelpRegionHome => String get repeater_cliHelpRegionHome =>
@@ -2359,7 +2352,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get repeater_cliHelpGps => String get repeater_cliHelpGps =>
'Mostra lo stato del GPS. Quando il GPS è spento, risponde solo \"spento\", se è acceso risponde con \"acceso\", \"stato\", \"fix\" e numero di satelliti.'; 'Mostra lo stato del GPS. Quando il GPS è spento, risponde solo \"spento\", se è acceso risponde con \"acceso\", \"stato\", \"fix\" e numero di satelliti.';
@override @override
String get repeater_cliHelpGpsOnOff => String get repeater_cliHelpGpsOnOff =>
@@ -2416,7 +2409,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get repeater_gpsNote => String get repeater_gpsNote =>
'è stata introdotta una funzione gps per gestire le tematiche relative alla posizione.'; 'è stata introdotta una funzione gps per gestire le tematiche relative alla posizione.';
@override @override
String get telemetry_receivedData => 'Dati Telemetria Ricevuti'; String get telemetry_receivedData => 'Dati Telemetria Ricevuti';
@@ -2469,7 +2462,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String telemetry_temperatureValue(String celsius, String fahrenheit) { String telemetry_temperatureValue(String celsius, String fahrenheit) {
return '$celsius°C / $fahrenheit°F'; return '$celsius°C / $fahrenheit°F';
} }
@override @override
@@ -2537,7 +2530,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String channelPath_observedPathTitle(int index, String hops) { String channelPath_observedPathTitle(int index, String hops) {
return 'Percorso osservato $index $hops'; return 'Percorso osservato $index • $hops';
} }
@override @override
@@ -2592,7 +2585,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String channelPath_selectedPathLabel(String label, String prefixes) { String channelPath_selectedPathLabel(String label, String prefixes) {
return '$label $prefixes'; return '$label • $prefixes';
} }
@override @override
@@ -2603,14 +2596,14 @@ class AppLocalizationsIt extends AppLocalizations {
String get channelPath_unknownRepeater => 'Ripetitore sconosciuto'; String get channelPath_unknownRepeater => 'Ripetitore sconosciuto';
@override @override
String get community_title => 'Comunità'; String get community_title => 'Comunità';
@override @override
String get community_create => 'Crea Comunità'; String get community_create => 'Crea Comunità';
@override @override
String get community_createDesc => String get community_createDesc =>
'Crea una nuova comunità e condividila tramite codice QR.'; 'Crea una nuova comunità e condividila tramite codice QR.';
@override @override
String get community_join => 'Unisciti'; String get community_join => 'Unisciti';
@@ -2628,35 +2621,35 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get community_scanInstructions => String get community_scanInstructions =>
'Punta la fotocamera su un codice QR della comunità'; 'Punta la fotocamera su un codice QR della comunità';
@override @override
String get community_showQr => 'Mostra il codice QR'; String get community_showQr => 'Mostra il codice QR';
@override @override
String get community_publicChannel => 'Comunità Pubblica'; String get community_publicChannel => 'Comunità Pubblica';
@override @override
String get community_hashtagChannel => 'Hashtag della Comunità'; String get community_hashtagChannel => 'Hashtag della Comunità';
@override @override
String get community_name => 'Nome della Comunità'; String get community_name => 'Nome della Comunità';
@override @override
String get community_enterName => 'Inserisci il nome della comunità'; String get community_enterName => 'Inserisci il nome della comunità';
@override @override
String community_created(String name) { String community_created(String name) {
return 'Comunità \"$name\" creata'; return 'Comunità \"$name\" creata';
} }
@override @override
String community_joined(String name) { String community_joined(String name) {
return 'Unito alla comunità \"$name\"'; return 'Unito alla comunità \"$name\"';
} }
@override @override
String get community_qrTitle => 'Condividi Comunità'; String get community_qrTitle => 'Condividi Comunità';
@override @override
String community_qrInstructions(String name) { String community_qrInstructions(String name) {
@@ -2671,16 +2664,16 @@ class AppLocalizationsIt extends AppLocalizations {
String get community_invalidQrCode => 'Codice QR della community non valido'; String get community_invalidQrCode => 'Codice QR della community non valido';
@override @override
String get community_alreadyMember => 'Già membro'; String get community_alreadyMember => 'Già membro';
@override @override
String community_alreadyMemberMessage(String name) { String community_alreadyMemberMessage(String name) {
return 'Sei già un membro di \"$name\".'; return 'Sei già un membro di \"$name\".';
} }
@override @override
String get community_addPublicChannel => String get community_addPublicChannel =>
'Aggiungi Canale Pubblico della Comunità'; 'Aggiungi Canale Pubblico della Comunità';
@override @override
String get community_addPublicChannelHint => String get community_addPublicChannelHint =>
@@ -2694,10 +2687,10 @@ class AppLocalizationsIt extends AppLocalizations {
'Scansiona un codice QR o crea una community per iniziare.'; 'Scansiona un codice QR o crea una community per iniziare.';
@override @override
String get community_manageCommunities => 'Gestisci Comunità'; String get community_manageCommunities => 'Gestisci Comunità';
@override @override
String get community_delete => 'Lascia la Comunità'; String get community_delete => 'Lascia la Comunità';
@override @override
String community_deleteConfirm(String name) { String community_deleteConfirm(String name) {
@@ -2706,12 +2699,12 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String community_deleteChannelsWarning(int count) { String community_deleteChannelsWarning(int count) {
return 'Questo eliminerà anche $count canale/i e i loro messaggi.'; return 'Questo eliminerà anche $count canale/i e i loro messaggi.';
} }
@override @override
String community_deleted(String name) { String community_deleted(String name) {
return 'Hai lasciato la comunità \"$name\"'; return 'Hai lasciato la comunità \"$name\"';
} }
@override @override
@@ -2751,21 +2744,21 @@ class AppLocalizationsIt extends AppLocalizations {
'Aggiungi un canale con hashtag per questa community'; 'Aggiungi un canale con hashtag per questa community';
@override @override
String get community_selectCommunity => 'Seleziona Comunità'; String get community_selectCommunity => 'Seleziona Comunità';
@override @override
String get community_regularHashtag => 'Hashtag regolare'; String get community_regularHashtag => 'Hashtag regolare';
@override @override
String get community_regularHashtagDesc => String get community_regularHashtagDesc =>
'Hashtag pubblico (chiunque può unirsi)'; 'Hashtag pubblico (chiunque può unirsi)';
@override @override
String get community_communityHashtag => 'Hashtag della Comunità'; String get community_communityHashtag => 'Hashtag della Comunità';
@override @override
String get community_communityHashtagDesc => String get community_communityHashtagDesc =>
'Visibile solo ai membri della comunità'; 'Visibile solo ai membri della comunità';
@override @override
String community_forCommunity(String name) { String community_forCommunity(String name) {
@@ -2832,7 +2825,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get pathTrace_someHopsNoLocation => String get pathTrace_someHopsNoLocation =>
'Uno o più dei luppoli mancano di una posizione!'; 'Uno o più dei luppoli mancano di una posizione!';
@override @override
String get pathTrace_clearTooltip => 'Pulisci percorso'; String get pathTrace_clearTooltip => 'Pulisci percorso';
@@ -2854,7 +2847,7 @@ class AppLocalizationsIt extends AppLocalizations {
'Eseguire LOS per visualizzare il profilo altimetrico'; 'Eseguire LOS per visualizzare il profilo altimetrico';
@override @override
String get losMenuTitle => 'Menù LOS'; String get losMenuTitle => 'Menù LOS';
@override @override
String get losMenuSubtitle => String get losMenuSubtitle =>
@@ -2926,7 +2919,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get losErrorElevationUnavailable => String get losErrorElevationUnavailable =>
'Dati di elevazione non disponibili per uno o più campioni.'; 'Dati di elevazione non disponibili per uno o più campioni.';
@override @override
String get losErrorInvalidInput => String get losErrorInvalidInput =>
@@ -2964,7 +2957,7 @@ class AppLocalizationsIt extends AppLocalizations {
String get losFrequencyInfoTooltip => 'Visualizza i dettagli del calcolo'; String get losFrequencyInfoTooltip => 'Visualizza i dettagli del calcolo';
@override @override
String get losFrequencyDialogTitle => 'Calcolo dellorizzonte radio'; String get losFrequencyDialogTitle => 'Calcolo dell’orizzonte radio';
@override @override
String losFrequencyDialogDescription( String losFrequencyDialogDescription(
@@ -3004,13 +2997,13 @@ class AppLocalizationsIt extends AppLocalizations {
} }
@override @override
String get contacts_clipboardEmpty => 'La clipboard è vuota.'; String get contacts_clipboardEmpty => 'La clipboard è vuota.';
@override @override
String get contacts_invalidAdvertFormat => 'Dati di contatto non validi'; String get contacts_invalidAdvertFormat => 'Dati di contatto non validi';
@override @override
String get contacts_contactImported => 'Il contatto è stato importato.'; String get contacts_contactImported => 'Il contatto è stato importato.';
@override @override
String get contacts_contactImportFailed => String get contacts_contactImportFailed =>
@@ -3052,7 +3045,7 @@ class AppLocalizationsIt extends AppLocalizations {
'Copia dell\'annuncio nella Clipboard non riuscita.'; 'Copia dell\'annuncio nella Clipboard non riuscita.';
@override @override
String get notification_activityTitle => 'Attività MeshCore'; String get notification_activityTitle => 'Attività MeshCore';
@override @override
String notification_messagesCount(int count) { String notification_messagesCount(int count) {
@@ -3130,7 +3123,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get settings_gpxExportError => String get settings_gpxExportError =>
'Si è verificato un errore durante l\'esportazione.'; 'Si è verificato un errore durante l\'esportazione.';
@override @override
String get settings_gpxExportRepeatersRoom => String get settings_gpxExportRepeatersRoom =>
+35 -41
View File
@@ -69,7 +69,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get common_share => 'Delen'; String get common_share => 'Delen';
@override @override
String get common_copy => 'Kopiëren'; String get common_copy => 'Kopiëren';
@override @override
String get common_retry => 'Nogmaals proberen'; String get common_retry => 'Nogmaals proberen';
@@ -93,7 +93,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get common_loading => 'Laden...'; String get common_loading => 'Laden...';
@override @override
String get common_notAvailable => ''; String get common_notAvailable => '—';
@override @override
String common_voltageValue(String volts) { String common_voltageValue(String volts) {
@@ -108,13 +108,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get scanner_title => 'MeshCore Open'; String get scanner_title => 'MeshCore Open';
@override
String get connectionChoiceTitle => 'Kies uw verbindingsmethode';
@override
String get connectionChoiceSubtitle =>
'Kies hoe u uw MeshCore-apparaat wilt bereiken.';
@override @override
String get connectionChoiceUsbLabel => 'USB'; String get connectionChoiceUsbLabel => 'USB';
@@ -126,7 +119,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get usbScreenSubtitle => String get usbScreenSubtitle =>
'Kies een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.'; 'Kies een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.';
@override @override
String get usbScreenStatus => 'Selecteer een USB-apparaat'; String get usbScreenStatus => 'Selecteer een USB-apparaat';
@@ -238,7 +231,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_location => 'Locatie'; String get settings_location => 'Locatie';
@override @override
String get settings_locationSubtitle => 'GPS coördinaten'; String get settings_locationSubtitle => 'GPS coördinaten';
@override @override
String get settings_locationUpdated => 'Locatie bijgewerkt'; String get settings_locationUpdated => 'Locatie bijgewerkt';
@@ -457,10 +450,10 @@ class AppLocalizationsNl extends AppLocalizations {
String get appSettings_languageEn => 'English'; String get appSettings_languageEn => 'English';
@override @override
String get appSettings_languageFr => 'Français'; String get appSettings_languageFr => 'Français';
@override @override
String get appSettings_languageEs => 'Español'; String get appSettings_languageEs => 'Español';
@override @override
String get appSettings_languageDe => 'Deutsch'; String get appSettings_languageDe => 'Deutsch';
@@ -469,16 +462,16 @@ class AppLocalizationsNl extends AppLocalizations {
String get appSettings_languagePl => 'Polski'; String get appSettings_languagePl => 'Polski';
@override @override
String get appSettings_languageSl => 'Slovenščina'; String get appSettings_languageSl => 'Slovenščina';
@override @override
String get appSettings_languagePt => 'Português'; String get appSettings_languagePt => 'Português';
@override @override
String get appSettings_languageIt => 'Italiano'; String get appSettings_languageIt => 'Italiano';
@override @override
String get appSettings_languageZh => '中文'; String get appSettings_languageZh => '中文';
@override @override
String get appSettings_languageSv => 'Svenska'; String get appSettings_languageSv => 'Svenska';
@@ -487,16 +480,16 @@ class AppLocalizationsNl extends AppLocalizations {
String get appSettings_languageNl => 'Nederlands'; String get appSettings_languageNl => 'Nederlands';
@override @override
String get appSettings_languageSk => 'Slovenčina'; String get appSettings_languageSk => 'Slovenčina';
@override @override
String get appSettings_languageBg => 'Български'; String get appSettings_languageBg => 'Български';
@override @override
String get appSettings_languageRu => 'Russisch'; String get appSettings_languageRu => 'Russisch';
@override @override
String get appSettings_languageUk => 'Oekraïens'; String get appSettings_languageUk => 'Oekraïens';
@override @override
String get appSettings_enableMessageTracing => 'Berichttracking inschakelen'; String get appSettings_enableMessageTracing => 'Berichttracking inschakelen';
@@ -854,7 +847,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get channels_public => 'Openbaar'; String get channels_public => 'Openbaar';
@override @override
String get channels_private => 'Privé'; String get channels_private => 'Privé';
@override @override
String get channels_publicChannel => 'Open kanaal'; String get channels_publicChannel => 'Open kanaal';
@@ -954,14 +947,14 @@ class AppLocalizationsNl extends AppLocalizations {
String get channels_sortUnread => 'Ongelezen'; String get channels_sortUnread => 'Ongelezen';
@override @override
String get channels_createPrivateChannel => 'Maak een Privé Kanaal'; String get channels_createPrivateChannel => 'Maak een Privé Kanaal';
@override @override
String get channels_createPrivateChannelDesc => String get channels_createPrivateChannelDesc =>
'Beveiligd met een geheime sleutel.'; 'Beveiligd met een geheime sleutel.';
@override @override
String get channels_joinPrivateChannel => 'Sluit een Privé Kanaal aan'; String get channels_joinPrivateChannel => 'Sluit een Privé Kanaal aan';
@override @override
String get channels_joinPrivateChannelDesc => String get channels_joinPrivateChannelDesc =>
@@ -1343,7 +1336,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get map_nodesNeedGps => String get map_nodesNeedGps =>
'Nodes moeten hun GPS-coördinaten delen\nom op de kaart te verschijnen'; 'Nodes moeten hun GPS-coördinaten delen\nom op de kaart te verschijnen';
@override @override
String map_nodesCount(int count) { String map_nodesCount(int count) {
@@ -1371,7 +1364,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get map_pinDm => 'Verzenden als bericht (DM)'; String get map_pinDm => 'Verzenden als bericht (DM)';
@override @override
String get map_pinPrivate => 'Beveiligd (Privé)'; String get map_pinPrivate => 'Beveiligd (Privé)';
@override @override
String get map_pinPublic => 'Openbaar spikken'; String get map_pinPublic => 'Openbaar spikken';
@@ -2038,7 +2031,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get repeater_eraseSerialOnly => String get repeater_eraseSerialOnly =>
'Verwijderen is alleen beschikbaar via de seriële console.'; 'Verwijderen is alleen beschikbaar via de seriële console.';
@override @override
String repeater_commandSent(String command) { String repeater_commandSent(String command) {
@@ -2266,7 +2259,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get repeater_cliHelpSetBridgeBaud => String get repeater_cliHelpSetBridgeBaud =>
'Stel de seriële link baudrate in voor rs232 bruggen.'; 'Stel de seriële link baudrate in voor rs232 bruggen.';
@override @override
String get repeater_cliHelpSetBridgeSecret => String get repeater_cliHelpSetBridgeSecret =>
@@ -2282,7 +2275,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get repeater_cliHelpSetPerm => String get repeater_cliHelpSetPerm =>
'Wijzigt de ACL. Verwijder de overeenkomstige entry (door pubkey prefix) als \"permissions\" 0 is. Voeg een nieuwe entry toe als pubkey-hex volledig is en niet momenteel in de ACL staat. Update de entry door matching pubkey prefix. Toestemming bits variëren per firmware rol, maar de onderste 2 bits zijn: 0 (Gast), 1 (Alleen lezen), 2 (Lezen/schrijven), 3 (Admin)'; 'Wijzigt de ACL. Verwijder de overeenkomstige entry (door pubkey prefix) als \"permissions\" 0 is. Voeg een nieuwe entry toe als pubkey-hex volledig is en niet momenteel in de ACL staat. Update de entry door matching pubkey prefix. Toestemming bits variëren per firmware rol, maar de onderste 2 bits zijn: 0 (Gast), 1 (Alleen lezen), 2 (Lezen/schrijven), 3 (Admin)';
@override @override
String get repeater_cliHelpGetBridgeType => String get repeater_cliHelpGetBridgeType =>
@@ -2314,7 +2307,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get repeater_cliHelpRegionLoad => String get repeater_cliHelpRegionLoad =>
'LET OP: dit is een speciale multi-command aanroep. Elke volgende opdracht is een regiortaak (uitgelijnd met spaties om de ouderhiërarchie aan te duiden, met minimaal één spatie). Beëindigd door een lege regel/opdracht te sturen.'; 'LET OP: dit is een speciale multi-command aanroep. Elke volgende opdracht is een regiortaak (uitgelijnd met spaties om de ouderhiërarchie aan te duiden, met minimaal één spatie). Beëindigd door een lege regel/opdracht te sturen.';
@override @override
String get repeater_cliHelpRegionGet => String get repeater_cliHelpRegionGet =>
@@ -2359,7 +2352,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get repeater_cliHelpGpsSetLoc => String get repeater_cliHelpGpsSetLoc =>
'Stel de positie van de node vast als GPS-coördinaten en sla de voorkeuren op.'; 'Stel de positie van de node vast als GPS-coördinaten en sla de voorkeuren op.';
@override @override
String get repeater_cliHelpGpsAdvert => String get repeater_cliHelpGpsAdvert =>
@@ -2397,14 +2390,14 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get repeater_regionNote => String get repeater_regionNote =>
'Regio-commando\'s zijn geïntroduceerd om regio-definities en permissies te beheren.'; 'Regio-commando\'s zijn geïntroduceerd om regio-definities en permissies te beheren.';
@override @override
String get repeater_gpsManagement => 'Beheer GPS'; String get repeater_gpsManagement => 'Beheer GPS';
@override @override
String get repeater_gpsNote => String get repeater_gpsNote =>
'De GPS-commando is geïntroduceerd om locatiegerelateerde onderwerpen te beheren.'; 'De GPS-commando is geïntroduceerd om locatiegerelateerde onderwerpen te beheren.';
@override @override
String get telemetry_receivedData => 'Ontvangen Telemetriedata'; String get telemetry_receivedData => 'Ontvangen Telemetriedata';
@@ -2457,7 +2450,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String telemetry_temperatureValue(String celsius, String fahrenheit) { String telemetry_temperatureValue(String celsius, String fahrenheit) {
return '$celsius°C / $fahrenheit°F'; return '$celsius°C / $fahrenheit°F';
} }
@override @override
@@ -2526,7 +2519,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String channelPath_observedPathTitle(int index, String hops) { String channelPath_observedPathTitle(int index, String hops) {
return 'Waargenomen pad $index $hops'; return 'Waargenomen pad $index • $hops';
} }
@override @override
@@ -2581,7 +2574,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String channelPath_selectedPathLabel(String label, String prefixes) { String channelPath_selectedPathLabel(String label, String prefixes) {
return '$label $prefixes'; return '$label • $prefixes';
} }
@override @override
@@ -2998,11 +2991,11 @@ class AppLocalizationsNl extends AppLocalizations {
String get contacts_invalidAdvertFormat => 'Ongeldige contactgegevens'; String get contacts_invalidAdvertFormat => 'Ongeldige contactgegevens';
@override @override
String get contacts_contactImported => 'Contact is geïmporteerd.'; String get contacts_contactImported => 'Contact is geïmporteerd.';
@override @override
String get contacts_contactImportFailed => String get contacts_contactImportFailed =>
'Contact kon niet geïmporteerd worden.'; 'Contact kon niet geïmporteerd worden.';
@override @override
String get contacts_zeroHopAdvert => 'Zero Hop Reclame'; String get contacts_zeroHopAdvert => 'Zero Hop Reclame';
@@ -3011,14 +3004,14 @@ class AppLocalizationsNl extends AppLocalizations {
String get contacts_floodAdvert => 'Overstromingsadvertentie'; String get contacts_floodAdvert => 'Overstromingsadvertentie';
@override @override
String get contacts_copyAdvertToClipboard => 'Advert naar klembord kopiëren'; String get contacts_copyAdvertToClipboard => 'Advert naar klembord kopiëren';
@override @override
String get contacts_addContactFromClipboard => String get contacts_addContactFromClipboard =>
'Contact uit klembord toevoegen'; 'Contact uit klembord toevoegen';
@override @override
String get contacts_ShareContact => 'Kontakt naar Klembord kopiëren'; String get contacts_ShareContact => 'Kontakt naar Klembord kopiëren';
@override @override
String get contacts_ShareContactZeroHop => 'Contact delen via advertentie'; String get contacts_ShareContactZeroHop => 'Contact delen via advertentie';
@@ -3037,7 +3030,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get contacts_contactAdvertCopyFailed => String get contacts_contactAdvertCopyFailed =>
'Kopiëren van advertentie naar Clipboard is mislukt.'; 'Kopiëren van advertentie naar Clipboard is mislukt.';
@override @override
String get notification_activityTitle => 'MeshCore Activiteit'; String get notification_activityTitle => 'MeshCore Activiteit';
@@ -3106,7 +3099,8 @@ class AppLocalizationsNl extends AppLocalizations {
'Exporteert alle contacten met een locatie naar een GPX-bestand.'; 'Exporteert alle contacten met een locatie naar een GPX-bestand.';
@override @override
String get settings_gpxExportSuccess => 'Succesvol GPX-bestand geëxporteerd.'; String get settings_gpxExportSuccess =>
'Succesvol GPX-bestand geëxporteerd.';
@override @override
String get settings_gpxExportNoContacts => 'Geen contacten om te exporteren.'; String get settings_gpxExportNoContacts => 'Geen contacten om te exporteren.';
@@ -3130,7 +3124,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get settings_gpxExportShareText => String get settings_gpxExportShareText =>
'Kaartgegevens geëxporteerd uit meshcore-open'; 'Kaartgegevens geëxporteerd uit meshcore-open';
@override @override
String get settings_gpxExportShareSubject => String get settings_gpxExportShareSubject =>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+35 -37
View File
@@ -1,4 +1,4 @@
{ {
"channels_channelDeleteFailed": "Kan kanaal {name} niet verwijderen", "channels_channelDeleteFailed": "Kan kanaal {name} niet verwijderen",
"@channels_channelDeleteFailed": { "@channels_channelDeleteFailed": {
"placeholders": { "placeholders": {
@@ -27,7 +27,7 @@
"common_create": "Maak", "common_create": "Maak",
"common_continue": "Doorgaan", "common_continue": "Doorgaan",
"common_share": "Delen", "common_share": "Delen",
"common_copy": "Kopiëren", "common_copy": "Kopiëren",
"common_retry": "Nogmaals proberen", "common_retry": "Nogmaals proberen",
"common_hide": "Verbergen", "common_hide": "Verbergen",
"common_remove": "Verwijderen", "common_remove": "Verwijderen",
@@ -35,7 +35,7 @@
"common_disable": "Uitschakelen", "common_disable": "Uitschakelen",
"common_reboot": "Herstarten", "common_reboot": "Herstarten",
"common_loading": "Laden...", "common_loading": "Laden...",
"common_notAvailable": "", "common_notAvailable": "—",
"common_voltageValue": "{volts} V", "common_voltageValue": "{volts} V",
"@common_voltageValue": { "@common_voltageValue": {
"placeholders": { "placeholders": {
@@ -92,7 +92,7 @@
"settings_radioSettingsSubtitle": "Frequentie, vermogen, spredfactor", "settings_radioSettingsSubtitle": "Frequentie, vermogen, spredfactor",
"settings_radioSettingsUpdated": "Radio instellingen bijgewerkt", "settings_radioSettingsUpdated": "Radio instellingen bijgewerkt",
"settings_location": "Locatie", "settings_location": "Locatie",
"settings_locationSubtitle": "GPS coördinaten", "settings_locationSubtitle": "GPS coördinaten",
"settings_locationUpdated": "Locatie bijgewerkt", "settings_locationUpdated": "Locatie bijgewerkt",
"settings_locationBothRequired": "Voer zowel breedte- als lengtegraad in.", "settings_locationBothRequired": "Voer zowel breedte- als lengtegraad in.",
"settings_locationInvalid": "Ongeldige breedtegraad of lengtegraad.", "settings_locationInvalid": "Ongeldige breedtegraad of lengtegraad.",
@@ -165,18 +165,18 @@
"appSettings_language": "Taal", "appSettings_language": "Taal",
"appSettings_languageSystem": "Standaardinstelling", "appSettings_languageSystem": "Standaardinstelling",
"appSettings_languageEn": "English", "appSettings_languageEn": "English",
"appSettings_languageFr": "Français", "appSettings_languageFr": "Français",
"appSettings_languageEs": "Español", "appSettings_languageEs": "Español",
"appSettings_languageDe": "Deutsch", "appSettings_languageDe": "Deutsch",
"appSettings_languagePl": "Polski", "appSettings_languagePl": "Polski",
"appSettings_languageSl": "Slovenščina", "appSettings_languageSl": "Slovenščina",
"appSettings_languagePt": "Português", "appSettings_languagePt": "Português",
"appSettings_languageIt": "Italiano", "appSettings_languageIt": "Italiano",
"appSettings_languageZh": "中文", "appSettings_languageZh": "中文",
"appSettings_languageSv": "Svenska", "appSettings_languageSv": "Svenska",
"appSettings_languageNl": "Nederlands", "appSettings_languageNl": "Nederlands",
"appSettings_languageSk": "Slovenčina", "appSettings_languageSk": "Slovenčina",
"appSettings_languageBg": "Български", "appSettings_languageBg": "Български",
"appSettings_notifications": "Notificaties", "appSettings_notifications": "Notificaties",
"appSettings_enableNotifications": "Notificaties inschakelen", "appSettings_enableNotifications": "Notificaties inschakelen",
"appSettings_enableNotificationsSubtitle": "Ontvang meldingen voor berichten en advertenties", "appSettings_enableNotificationsSubtitle": "Ontvang meldingen voor berichten en advertenties",
@@ -338,7 +338,7 @@
}, },
"channels_hashtagChannel": "Hashtag kanaal", "channels_hashtagChannel": "Hashtag kanaal",
"channels_public": "Openbaar", "channels_public": "Openbaar",
"channels_private": "Privé", "channels_private": "Privé",
"channels_publicChannel": "Open kanaal", "channels_publicChannel": "Open kanaal",
"channels_privateChannel": "Private kanaal", "channels_privateChannel": "Private kanaal",
"channels_editChannel": "Kanaal bewerken", "channels_editChannel": "Kanaal bewerken",
@@ -623,7 +623,7 @@
"chat_invalidLink": "Ongeldig linkformaat", "chat_invalidLink": "Ongeldig linkformaat",
"map_title": "Node Map", "map_title": "Node Map",
"map_noNodesWithLocation": "Geen nodes met locatiegegevens", "map_noNodesWithLocation": "Geen nodes met locatiegegevens",
"map_nodesNeedGps": "Nodes moeten hun GPS-coördinaten delen\nom op de kaart te verschijnen", "map_nodesNeedGps": "Nodes moeten hun GPS-coördinaten delen\nom op de kaart te verschijnen",
"map_nodesCount": "Nodes: {count}", "map_nodesCount": "Nodes: {count}",
"@map_nodesCount": { "@map_nodesCount": {
"placeholders": { "placeholders": {
@@ -645,7 +645,7 @@
"map_room": "Ruimte", "map_room": "Ruimte",
"map_sensor": "Sensor", "map_sensor": "Sensor",
"map_pinDm": "Verzenden als bericht (DM)", "map_pinDm": "Verzenden als bericht (DM)",
"map_pinPrivate": "Beveiligd (Privé)", "map_pinPrivate": "Beveiligd (Privé)",
"map_pinPublic": "Openbaar spikken", "map_pinPublic": "Openbaar spikken",
"map_lastSeen": "Laaste keer gezien", "map_lastSeen": "Laaste keer gezien",
"map_disconnectConfirm": "Ben je er zeker van dat je verbinding met dit apparaat wilt verbreken?", "map_disconnectConfirm": "Ben je er zeker van dat je verbinding met dit apparaat wilt verbreken?",
@@ -1039,7 +1039,7 @@
"repeater_eraseFileSystem": "Verwijder Besturingssysteem", "repeater_eraseFileSystem": "Verwijder Besturingssysteem",
"repeater_eraseFileSystemSubtitle": "Formateer het bestandsysteem van de repeater", "repeater_eraseFileSystemSubtitle": "Formateer het bestandsysteem van de repeater",
"repeater_eraseFileSystemConfirm": "WAARSCHUWING: Dit zal alle gegevens op de repeater wissen. Dit kan niet worden teruggedraaid!", "repeater_eraseFileSystemConfirm": "WAARSCHUWING: Dit zal alle gegevens op de repeater wissen. Dit kan niet worden teruggedraaid!",
"repeater_eraseSerialOnly": "Verwijderen is alleen beschikbaar via de seriële console.", "repeater_eraseSerialOnly": "Verwijderen is alleen beschikbaar via de seriële console.",
"repeater_commandSent": "Commando verzonden: {command}", "repeater_commandSent": "Commando verzonden: {command}",
"@repeater_commandSent": { "@repeater_commandSent": {
"placeholders": { "placeholders": {
@@ -1143,11 +1143,11 @@
"repeater_cliHelpSetBridgeEnabled": "Poort inschakelen/uitschakelen.", "repeater_cliHelpSetBridgeEnabled": "Poort inschakelen/uitschakelen.",
"repeater_cliHelpSetBridgeDelay": "Verzend vertraging instellen voor pakketten.", "repeater_cliHelpSetBridgeDelay": "Verzend vertraging instellen voor pakketten.",
"repeater_cliHelpSetBridgeSource": "Kies of de brug ontvangen pakketten of verzonden pakketten opnieuw moet versturen.", "repeater_cliHelpSetBridgeSource": "Kies of de brug ontvangen pakketten of verzonden pakketten opnieuw moet versturen.",
"repeater_cliHelpSetBridgeBaud": "Stel de seriële link baudrate in voor rs232 bruggen.", "repeater_cliHelpSetBridgeBaud": "Stel de seriële link baudrate in voor rs232 bruggen.",
"repeater_cliHelpSetBridgeSecret": "Stel bridge-geheim in voor espnow bridges.", "repeater_cliHelpSetBridgeSecret": "Stel bridge-geheim in voor espnow bridges.",
"repeater_cliHelpSetAdcMultiplier": "Stelt een aangepaste factor in om de gerapporteerde batterijspanning aan te passen (alleen ondersteund op selecte borden).", "repeater_cliHelpSetAdcMultiplier": "Stelt een aangepaste factor in om de gerapporteerde batterijspanning aan te passen (alleen ondersteund op selecte borden).",
"repeater_cliHelpTempRadio": "Stelt tijdelijke radio parameters in voor het opgegeven aantal minuten, en keert daarna terug naar de originele radio parameters. (wordt niet opgeslagen in de voorkeuren).", "repeater_cliHelpTempRadio": "Stelt tijdelijke radio parameters in voor het opgegeven aantal minuten, en keert daarna terug naar de originele radio parameters. (wordt niet opgeslagen in de voorkeuren).",
"repeater_cliHelpSetPerm": "Wijzigt de ACL. Verwijder de overeenkomstige entry (door pubkey prefix) als \"permissions\" 0 is. Voeg een nieuwe entry toe als pubkey-hex volledig is en niet momenteel in de ACL staat. Update de entry door matching pubkey prefix. Toestemming bits variëren per firmware rol, maar de onderste 2 bits zijn: 0 (Gast), 1 (Alleen lezen), 2 (Lezen/schrijven), 3 (Admin)", "repeater_cliHelpSetPerm": "Wijzigt de ACL. Verwijder de overeenkomstige entry (door pubkey prefix) als \"permissions\" 0 is. Voeg een nieuwe entry toe als pubkey-hex volledig is en niet momenteel in de ACL staat. Update de entry door matching pubkey prefix. Toestemming bits variëren per firmware rol, maar de onderste 2 bits zijn: 0 (Gast), 1 (Alleen lezen), 2 (Lezen/schrijven), 3 (Admin)",
"repeater_cliHelpGetBridgeType": "Ontvang brugtype: geen, rs232, espnow", "repeater_cliHelpGetBridgeType": "Ontvang brugtype: geen, rs232, espnow",
"repeater_cliHelpLogStart": "Start pakketlogging naar het bestandssysteem.", "repeater_cliHelpLogStart": "Start pakketlogging naar het bestandssysteem.",
"repeater_cliHelpLogStop": "Stoppen met het loggen van pakketten naar het bestandssysteem.", "repeater_cliHelpLogStop": "Stoppen met het loggen van pakketten naar het bestandssysteem.",
@@ -1155,7 +1155,7 @@
"repeater_cliHelpNeighbors": "Toont een lijst met andere repeater nodes die via nul-hop advertenties zijn gehoord. Elke regel is id-prefix-hex:timestamp:snr-times-4", "repeater_cliHelpNeighbors": "Toont een lijst met andere repeater nodes die via nul-hop advertenties zijn gehoord. Elke regel is id-prefix-hex:timestamp:snr-times-4",
"repeater_cliHelpNeighborRemove": "Verwijdert de eerste overeenkomende vermelding (via pubkey prefix (hex)) uit de lijst van buren.", "repeater_cliHelpNeighborRemove": "Verwijdert de eerste overeenkomende vermelding (via pubkey prefix (hex)) uit de lijst van buren.",
"repeater_cliHelpRegion": "(Alleen Serieel) Lijst alle gedefinieerde regio's en huidige floodrechten.", "repeater_cliHelpRegion": "(Alleen Serieel) Lijst alle gedefinieerde regio's en huidige floodrechten.",
"repeater_cliHelpRegionLoad": "LET OP: dit is een speciale multi-command aanroep. Elke volgende opdracht is een regiortaak (uitgelijnd met spaties om de ouderhiërarchie aan te duiden, met minimaal één spatie). Beëindigd door een lege regel/opdracht te sturen.", "repeater_cliHelpRegionLoad": "LET OP: dit is een speciale multi-command aanroep. Elke volgende opdracht is een regiortaak (uitgelijnd met spaties om de ouderhiërarchie aan te duiden, met minimaal één spatie). Beëindigd door een lege regel/opdracht te sturen.",
"repeater_cliHelpRegionGet": "Zoekt naar regio met gegeven naam voorvoegsel (of \"\" voor de globale scope). Antwoordt met \"-> regio-naam (ouder-naam) 'F'\"", "repeater_cliHelpRegionGet": "Zoekt naar regio met gegeven naam voorvoegsel (of \"\" voor de globale scope). Antwoordt met \"-> regio-naam (ouder-naam) 'F'\"",
"repeater_cliHelpRegionPut": "Voegt of wijzigt een regio-definitie met de gegeven naam.", "repeater_cliHelpRegionPut": "Voegt of wijzigt een regio-definitie met de gegeven naam.",
"repeater_cliHelpRegionRemove": "Verwijdert een regio-definitie met de gegeven naam. (moet exact overeenkomen en geen kindregio's hebben)", "repeater_cliHelpRegionRemove": "Verwijdert een regio-definitie met de gegeven naam. (moet exact overeenkomen en geen kindregio's hebben)",
@@ -1167,7 +1167,7 @@
"repeater_cliHelpGps": "Geeft de status van de GPS. Wanneer de GPS uit staat, antwoordt het alleen met \"uit\", als het aan staat, antwoordt het met \"aan\", status, fix, sat count.", "repeater_cliHelpGps": "Geeft de status van de GPS. Wanneer de GPS uit staat, antwoordt het alleen met \"uit\", als het aan staat, antwoordt het met \"aan\", status, fix, sat count.",
"repeater_cliHelpGpsOnOff": "Schakel de GPS-standby aan/uit.", "repeater_cliHelpGpsOnOff": "Schakel de GPS-standby aan/uit.",
"repeater_cliHelpGpsSync": "Synchroniseer node met GPS-klok.", "repeater_cliHelpGpsSync": "Synchroniseer node met GPS-klok.",
"repeater_cliHelpGpsSetLoc": "Stel de positie van de node vast als GPS-coördinaten en sla de voorkeuren op.", "repeater_cliHelpGpsSetLoc": "Stel de positie van de node vast als GPS-coördinaten en sla de voorkeuren op.",
"repeater_cliHelpGpsAdvert": "Geeft de locatie advertentieconfiguratie van de node:\n- none: locatie niet in advertenties opnemen\n- share: gps locatie delen (van SensorManager)\n- prefs: locatie adverteren die in de voorkeuren is opgeslagen", "repeater_cliHelpGpsAdvert": "Geeft de locatie advertentieconfiguratie van de node:\n- none: locatie niet in advertenties opnemen\n- share: gps locatie delen (van SensorManager)\n- prefs: locatie adverteren die in de voorkeuren is opgeslagen",
"repeater_cliHelpGpsAdvertSet": "Stelt advertentie locatie configuratie in.", "repeater_cliHelpGpsAdvertSet": "Stelt advertentie locatie configuratie in.",
"repeater_commandsListTitle": "Commandenlijst", "repeater_commandsListTitle": "Commandenlijst",
@@ -1178,9 +1178,9 @@
"repeater_logging": "Logging", "repeater_logging": "Logging",
"repeater_neighborsRepeaterOnly": "Buren (Alleen repeaters)", "repeater_neighborsRepeaterOnly": "Buren (Alleen repeaters)",
"repeater_regionManagementRepeaterOnly": "Regiobeheer (Alleen Repeater)", "repeater_regionManagementRepeaterOnly": "Regiobeheer (Alleen Repeater)",
"repeater_regionNote": "Regio-commando's zijn geïntroduceerd om regio-definities en permissies te beheren.", "repeater_regionNote": "Regio-commando's zijn geïntroduceerd om regio-definities en permissies te beheren.",
"repeater_gpsManagement": "Beheer GPS", "repeater_gpsManagement": "Beheer GPS",
"repeater_gpsNote": "De GPS-commando is geïntroduceerd om locatiegerelateerde onderwerpen te beheren.", "repeater_gpsNote": "De GPS-commando is geïntroduceerd om locatiegerelateerde onderwerpen te beheren.",
"telemetry_receivedData": "Ontvangen Telemetriedata", "telemetry_receivedData": "Ontvangen Telemetriedata",
"telemetry_requestTimeout": "Telemetryverzoek is uitgevallen.", "telemetry_requestTimeout": "Telemetryverzoek is uitgevallen.",
"telemetry_errorLoading": "Fout bij het laden van de telemetrie: {error}", "telemetry_errorLoading": "Fout bij het laden van de telemetrie: {error}",
@@ -1232,7 +1232,7 @@
} }
} }
}, },
"telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F",
"@telemetry_temperatureValue": { "@telemetry_temperatureValue": {
"placeholders": { "placeholders": {
"celsius": { "celsius": {
@@ -1254,7 +1254,7 @@
"channelPath_repeatsLabel": "Repeats", "channelPath_repeatsLabel": "Repeats",
"channelPath_pathLabel": "Pad {index}", "channelPath_pathLabel": "Pad {index}",
"channelPath_observedLabel": "Waargenomen", "channelPath_observedLabel": "Waargenomen",
"channelPath_observedPathTitle": "Waargenomen pad {index} {hops}", "channelPath_observedPathTitle": "Waargenomen pad {index} • {hops}",
"@channelPath_observedPathTitle": { "@channelPath_observedPathTitle": {
"placeholders": { "placeholders": {
"index": { "index": {
@@ -1329,7 +1329,7 @@
}, },
"channelPath_pathLabelTitle": "Pad", "channelPath_pathLabelTitle": "Pad",
"channelPath_observedPathHeader": "Waargenomen Pad", "channelPath_observedPathHeader": "Waargenomen Pad",
"channelPath_selectedPathLabel": "{label} {prefixes}", "channelPath_selectedPathLabel": "{label} • {prefixes}",
"@channelPath_selectedPathLabel": { "@channelPath_selectedPathLabel": {
"placeholders": { "placeholders": {
"label": { "label": {
@@ -1369,8 +1369,8 @@
"neighbors_repeatersNeighbors": "Herhalingen Buren", "neighbors_repeatersNeighbors": "Herhalingen Buren",
"neighbors_noData": "Geen gegevens van buren beschikbaar.", "neighbors_noData": "Geen gegevens van buren beschikbaar.",
"channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.", "channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.",
"channels_createPrivateChannel": "Maak een Privé Kanaal", "channels_createPrivateChannel": "Maak een Privé Kanaal",
"channels_joinPrivateChannel": "Sluit een Privé Kanaal aan", "channels_joinPrivateChannel": "Sluit een Privé Kanaal aan",
"channels_joinPrivateChannelDesc": "Handmatig een geheime sleutel invoeren.", "channels_joinPrivateChannelDesc": "Handmatig een geheime sleutel invoeren.",
"channels_joinPublicChannel": "Sluit het Open Kanaal", "channels_joinPublicChannel": "Sluit het Open Kanaal",
"channels_joinPublicChannelDesc": "Iedereen kan dit kanaal aanmelden.", "channels_joinPublicChannelDesc": "Iedereen kan dit kanaal aanmelden.",
@@ -1558,22 +1558,22 @@
"contacts_roomPing": "Ping kamer server", "contacts_roomPing": "Ping kamer server",
"contacts_chatTraceRoute": "Route traceren", "contacts_chatTraceRoute": "Route traceren",
"contacts_pathTraceTo": "Trace route to {name}", "contacts_pathTraceTo": "Trace route to {name}",
"appSettings_languageUk": "Oekraïens", "appSettings_languageUk": "Oekraïens",
"contacts_invalidAdvertFormat": "Ongeldige contactgegevens", "contacts_invalidAdvertFormat": "Ongeldige contactgegevens",
"contacts_contactImportFailed": "Contact kon niet geïmporteerd worden.", "contacts_contactImportFailed": "Contact kon niet geïmporteerd worden.",
"contacts_zeroHopAdvert": "Zero Hop Reclame", "contacts_zeroHopAdvert": "Zero Hop Reclame",
"contacts_floodAdvert": "Overstromingsadvertentie", "contacts_floodAdvert": "Overstromingsadvertentie",
"contacts_copyAdvertToClipboard": "Advert naar klembord kopiëren", "contacts_copyAdvertToClipboard": "Advert naar klembord kopiëren",
"appSettings_languageRu": "Russisch", "appSettings_languageRu": "Russisch",
"appSettings_enableMessageTracing": "Berichttracking inschakelen", "appSettings_enableMessageTracing": "Berichttracking inschakelen",
"appSettings_enableMessageTracingSubtitle": "Gedetailleerde routerings- en timing-metadata voor berichten weergeven", "appSettings_enableMessageTracingSubtitle": "Gedetailleerde routerings- en timing-metadata voor berichten weergeven",
"contacts_clipboardEmpty": "Knipbord is leeg.", "contacts_clipboardEmpty": "Knipbord is leeg.",
"contacts_addContactFromClipboard": "Contact uit klembord toevoegen", "contacts_addContactFromClipboard": "Contact uit klembord toevoegen",
"contacts_contactImported": "Contact is geïmporteerd.", "contacts_contactImported": "Contact is geïmporteerd.",
"contacts_zeroHopContactAdvertSent": "Contact verzonden via advertentie", "contacts_zeroHopContactAdvertSent": "Contact verzonden via advertentie",
"contacts_contactAdvertCopied": "Reclame gekopieerd naar Klembord.", "contacts_contactAdvertCopied": "Reclame gekopieerd naar Klembord.",
"contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.", "contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.",
"contacts_ShareContact": "Kontakt naar Klembord kopiëren", "contacts_ShareContact": "Kontakt naar Klembord kopiëren",
"contacts_ShareContactZeroHop": "Contact delen via advertentie", "contacts_ShareContactZeroHop": "Contact delen via advertentie",
"contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden", "contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden",
"notification_activityTitle": "MeshCore Activiteit", "notification_activityTitle": "MeshCore Activiteit",
@@ -1584,7 +1584,7 @@
"notification_receivedNewMessage": "Nieuw bericht ontvangen", "notification_receivedNewMessage": "Nieuw bericht ontvangen",
"settings_gpxExportRepeatersSubtitle": "Exporteert repeaters / roomserver met een locatie naar GPX-bestand.", "settings_gpxExportRepeatersSubtitle": "Exporteert repeaters / roomserver met een locatie naar GPX-bestand.",
"settings_gpxExportRepeaters": "Exporteer repeaters / roomserver naar GPX", "settings_gpxExportRepeaters": "Exporteer repeaters / roomserver naar GPX",
"settings_gpxExportSuccess": "Succesvol GPX-bestand geëxporteerd.", "settings_gpxExportSuccess": "Succesvol GPX-bestand geëxporteerd.",
"settings_gpxExportNoContacts": "Geen contacten om te exporteren.", "settings_gpxExportNoContacts": "Geen contacten om te exporteren.",
"settings_gpxExportNotAvailable": "Niet ondersteund op uw apparaat/besturingssysteem", "settings_gpxExportNotAvailable": "Niet ondersteund op uw apparaat/besturingssysteem",
"settings_gpxExportError": "Er was een fout bij het exporteren.", "settings_gpxExportError": "Er was een fout bij het exporteren.",
@@ -1595,7 +1595,7 @@
"settings_gpxExportRepeatersRoom": "Repeater- en kamer servers locaties", "settings_gpxExportRepeatersRoom": "Repeater- en kamer servers locaties",
"settings_gpxExportChat": "Locaties van metgezellen", "settings_gpxExportChat": "Locaties van metgezellen",
"settings_gpxExportAllContacts": "Alle contactlocaties", "settings_gpxExportAllContacts": "Alle contactlocaties",
"settings_gpxExportShareText": "Kaartgegevens geëxporteerd uit meshcore-open", "settings_gpxExportShareText": "Kaartgegevens geëxporteerd uit meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open GPX kaartgegevens exporteren", "settings_gpxExportShareSubject": "meshcore-open GPX kaartgegevens exporteren",
"pathTrace_someHopsNoLocation": "Een of meer van de hops ontbreken een locatie!", "pathTrace_someHopsNoLocation": "Een of meer van de hops ontbreken een locatie!",
"map_removeLast": "Verwijder Laatste", "map_removeLast": "Verwijder Laatste",
@@ -1802,11 +1802,9 @@
"contacts_searchUsers": "Zoek {number}{str} gebruikers...", "contacts_searchUsers": "Zoek {number}{str} gebruikers...",
"contacts_searchFavorites": "Zoek {number}{str} favorieten...", "contacts_searchFavorites": "Zoek {number}{str} favorieten...",
"contacts_searchRoomServers": "Zoek {number}{str} Room servers...", "contacts_searchRoomServers": "Zoek {number}{str} Room servers...",
"connectionChoiceTitle": "Kies uw verbindingsmethode",
"connectionChoiceUsbLabel": "USB", "connectionChoiceUsbLabel": "USB",
"connectionChoiceSubtitle": "Kies hoe u uw MeshCore-apparaat wilt bereiken.",
"connectionChoiceBluetoothLabel": "Bluetooth", "connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenSubtitle": "Kies een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.", "usbScreenSubtitle": "Kies een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.",
"usbScreenStatus": "Selecteer een USB-apparaat", "usbScreenStatus": "Selecteer een USB-apparaat",
"usbScreenNote": "USB-serieel is actief op ondersteunde Android-apparaten en desktop-platforms.", "usbScreenNote": "USB-serieel is actief op ondersteunde Android-apparaten en desktop-platforms.",
"usbScreenTitle": "Verbind via USB", "usbScreenTitle": "Verbind via USB",
+594 -596
View File
File diff suppressed because it is too large Load Diff
+385 -387
View File
File diff suppressed because it is too large Load Diff
+868 -870
View File
File diff suppressed because it is too large Load Diff
+726 -728
View File
File diff suppressed because it is too large Load Diff
+385 -387
View File
File diff suppressed because it is too large Load Diff
+448 -450
View File
File diff suppressed because it is too large Load Diff
+861 -863
View File
File diff suppressed because it is too large Load Diff
+865 -867
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -8,7 +8,7 @@ import 'screens/chrome_required_screen.dart';
import 'utils/platform_info.dart'; import 'utils/platform_info.dart';
import 'connector/meshcore_connector.dart'; import 'connector/meshcore_connector.dart';
import 'screens/connection_choice_screen.dart'; import 'screens/scanner_screen.dart';
import 'services/storage_service.dart'; import 'services/storage_service.dart';
import 'services/message_retry_service.dart'; import 'services/message_retry_service.dart';
import 'services/path_history_service.dart'; import 'services/path_history_service.dart';
@@ -192,7 +192,7 @@ class MeshCoreApp extends StatelessWidget {
}, },
home: (PlatformInfo.isWeb && !PlatformInfo.isChrome) home: (PlatformInfo.isWeb && !PlatformInfo.isChrome)
? const ChromeRequiredScreen() ? const ChromeRequiredScreen()
: const ConnectionChoiceScreen(), : const ScannerScreen(),
); );
}, },
), ),
-232
View File
@@ -1,232 +0,0 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import '../l10n/l10n.dart';
import '../utils/platform_info.dart';
import 'scanner_screen.dart';
import 'usb_screen.dart';
/// Entry point that lets the user choose between USB or Bluetooth.
class ConnectionChoiceScreen extends StatelessWidget {
const ConnectionChoiceScreen({super.key});
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final theme = Theme.of(context);
final usbSupported = PlatformInfo.supportsUsbSerial;
return Scaffold(
appBar: AppBar(
title: FittedBox(
fit: BoxFit.scaleDown,
child: Text(l10n.appTitle, textAlign: TextAlign.center),
),
centerTitle: true,
automaticallyImplyLeading: false,
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
child: LayoutBuilder(
builder: (context, constraints) {
final availableHeight = constraints.maxHeight.isFinite
? constraints.maxHeight
: 600.0;
final gap = math.max(
8.0,
math.min(20.0, availableHeight * 0.035),
);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Flexible(
flex: 3,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
l10n.connectionChoiceTitle,
textAlign: TextAlign.center,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
),
),
SizedBox(height: math.max(4.0, gap * 0.5)),
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
l10n.connectionChoiceSubtitle,
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
),
],
),
),
),
SizedBox(height: gap),
Expanded(
flex: 4,
child: _ConnectionMethodButton(
icon: Icons.usb,
label: l10n.connectionChoiceUsbLabel,
color: theme.colorScheme.primaryContainer,
iconColor: theme.colorScheme.onPrimaryContainer,
onPressed: usbSupported
? () {
debugPrint(
'ConnectionChoiceScreen: USB selected, opening UsbScreen',
);
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const UsbScreen(),
),
);
}
: null,
),
),
SizedBox(height: gap),
Expanded(
flex: 4,
child: _ConnectionMethodButton(
icon: Icons.bluetooth,
label: l10n.connectionChoiceBluetoothLabel,
color: theme.colorScheme.surfaceContainerHighest,
iconColor: theme.colorScheme.onSurfaceVariant,
onPressed: () {
debugPrint(
'ConnectionChoiceScreen: Bluetooth selected, opening ScannerScreen',
);
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const ScannerScreen(),
),
);
},
),
),
],
);
},
),
),
),
);
}
}
class _ConnectionMethodButton extends StatelessWidget {
const _ConnectionMethodButton({
required this.icon,
required this.label,
required this.onPressed,
required this.color,
required this.iconColor,
});
final IconData icon;
final String label;
final VoidCallback? onPressed;
final Color color;
final Color iconColor;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: color,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
minimumSize: const Size.fromHeight(0),
),
onPressed: onPressed,
child: LayoutBuilder(
builder: (context, constraints) {
final availableHeight = constraints.maxHeight.isFinite
? constraints.maxHeight
: 200.0;
final availableWidth = constraints.maxWidth.isFinite
? constraints.maxWidth
: 320.0;
final isCompact = availableHeight < 72.0 || availableWidth < 180.0;
final useTightVertical = !isCompact && availableHeight < 120.0;
final baseGap = isCompact
? 8.0
: (useTightVertical
? math.max(4.0, math.min(8.0, availableHeight * 0.06))
: 12.0);
final labelStyle =
(isCompact
? theme.textTheme.titleMedium
: (useTightVertical
? theme.textTheme.titleMedium
: theme.textTheme.titleLarge))
?.copyWith(fontWeight: FontWeight.w600);
final verticalIconSize = useTightVertical
? math.max(32.0, math.min(48.0, availableHeight * 0.42))
: 60.0;
final content = isCompact
? Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 24.0, color: iconColor),
SizedBox(width: baseGap),
Flexible(
child: Text(
label,
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: labelStyle,
),
),
],
)
: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: verticalIconSize, color: iconColor),
SizedBox(height: baseGap),
Text(
label,
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.visible,
style: labelStyle,
),
],
);
return Center(
child: FittedBox(
fit: BoxFit.scaleDown,
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: math.max(0, availableWidth - 12),
maxHeight: math.max(0, availableHeight - 12),
),
child: content,
),
),
);
},
),
);
}
}
+56 -28
View File
@@ -9,6 +9,7 @@ import '../l10n/l10n.dart';
import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/adaptive_app_bar_title.dart';
import '../widgets/device_tile.dart'; import '../widgets/device_tile.dart';
import 'contacts_screen.dart'; import 'contacts_screen.dart';
import 'usb_screen.dart';
/// Screen for scanning and connecting to MeshCore devices /// Screen for scanning and connecting to MeshCore devices
class ScannerScreen extends StatefulWidget { class ScannerScreen extends StatefulWidget {
@@ -114,40 +115,67 @@ class _ScannerScreenState extends State<ScannerScreen> {
}, },
), ),
), ),
floatingActionButton: Consumer<MeshCoreConnector>( bottomNavigationBar: Consumer<MeshCoreConnector>(
builder: (context, connector, child) { builder: (context, connector, child) {
final isScanning = final isScanning =
connector.state == MeshCoreConnectionState.scanning; connector.state == MeshCoreConnectionState.scanning;
final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off; final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off;
final usbSupported = PlatformInfo.supportsUsbSerial;
return FloatingActionButton.extended( return SafeArea(
onPressed: isBluetoothOff top: false,
? null minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
: () { child: Row(
if (isScanning) { mainAxisAlignment: MainAxisAlignment.end,
connector.stopScan(); children: [
} else { if (usbSupported)
unawaited( FloatingActionButton.extended(
connector.startScan().catchError((e) { onPressed: () {
debugPrint("Scanner screen startScan error: $e"); debugPrint(
}), 'ScannerScreen: USB selected, opening UsbScreen',
); );
} Navigator.of(context).push(
}, MaterialPageRoute(builder: (_) => const UsbScreen()),
icon: isScanning );
? const SizedBox( },
width: 20, heroTag: 'scanner_usb_action',
height: 20, icon: const Icon(Icons.usb),
child: CircularProgressIndicator( label: Text(context.l10n.connectionChoiceUsbLabel),
strokeWidth: 2, ),
color: Colors.white, if (usbSupported) const SizedBox(width: 12),
), FloatingActionButton.extended(
) heroTag: 'scanner_ble_action',
: const Icon(Icons.bluetooth_searching), onPressed: isBluetoothOff
label: Text( ? null
isScanning : () {
? context.l10n.scanner_stop if (isScanning) {
: context.l10n.scanner_scan, connector.stopScan();
} else {
unawaited(
connector.startScan().catchError((e) {
debugPrint(
"Scanner screen startScan error: $e",
);
}),
);
}
},
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,
),
),
],
), ),
); );
}, },
+38 -16
View File
@@ -5,10 +5,12 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart'; import '../connector/meshcore_connector.dart';
import '../connector/meshcore_connector_usb.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../utils/platform_info.dart'; import '../utils/platform_info.dart';
import '../utils/usb_port_labels.dart'; import '../utils/usb_port_labels.dart';
import 'contacts_screen.dart'; import 'contacts_screen.dart';
import 'scanner_screen.dart';
class UsbScreen extends StatefulWidget { class UsbScreen extends StatefulWidget {
const UsbScreen({super.key}); const UsbScreen({super.key});
@@ -28,6 +30,7 @@ class _UsbScreenState extends State<UsbScreen> {
String? _errorText; String? _errorText;
Timer? _hotPlugTimer; Timer? _hotPlugTimer;
late final MeshCoreConnector _connector; late final MeshCoreConnector _connector;
late final MeshCoreConnectorUsb _usbConnector;
late final VoidCallback _connectionListener; late final VoidCallback _connectionListener;
/// Whether the current platform supports dynamic hot-plug polling. /// Whether the current platform supports dynamic hot-plug polling.
@@ -40,12 +43,13 @@ class _UsbScreenState extends State<UsbScreen> {
void initState() { void initState() {
super.initState(); super.initState();
_connector = context.read<MeshCoreConnector>(); _connector = context.read<MeshCoreConnector>();
_usbConnector = MeshCoreConnectorUsb(_connector);
_connectionListener = () { _connectionListener = () {
if (!mounted) return; if (!mounted) return;
final activeUsbPortDisplayLabel = _connector.activeUsbPortDisplayLabel; final activeUsbPortDisplayLabel = _usbConnector.activeUsbPortDisplayLabel;
final shouldUpdateDisplayLabel = final shouldUpdateDisplayLabel =
activeUsbPortDisplayLabel != _connectedPortDisplayLabel; activeUsbPortDisplayLabel != _connectedPortDisplayLabel;
if (_connector.state == MeshCoreConnectionState.disconnected) { if (_usbConnector.state == MeshCoreConnectionState.disconnected) {
_navigatedToContacts = false; _navigatedToContacts = false;
setState(() { setState(() {
_isConnecting = false; _isConnecting = false;
@@ -56,8 +60,8 @@ class _UsbScreenState extends State<UsbScreen> {
_connectedPortDisplayLabel = activeUsbPortDisplayLabel; _connectedPortDisplayLabel = activeUsbPortDisplayLabel;
}); });
} }
if (_connector.state == MeshCoreConnectionState.connected && if (_usbConnector.state == MeshCoreConnectionState.connected &&
_connector.isUsbTransportConnected && _usbConnector.isUsbTransportConnected &&
!_navigatedToContacts) { !_navigatedToContacts) {
_navigatedToContacts = true; _navigatedToContacts = true;
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
@@ -65,14 +69,14 @@ class _UsbScreenState extends State<UsbScreen> {
); );
} }
}; };
_connector.addListener(_connectionListener); _usbConnector.addListener(_connectionListener);
_startHotPlugTimer(); _startHotPlugTimer();
} }
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
_connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus); _usbConnector.setRequestPortLabel(context.l10n.usbScreenStatus);
if (!_didScheduleInitialLoad) { if (!_didScheduleInitialLoad) {
_didScheduleInitialLoad = true; _didScheduleInitialLoad = true;
unawaited(_loadPorts()); unawaited(_loadPorts());
@@ -83,12 +87,12 @@ class _UsbScreenState extends State<UsbScreen> {
void dispose() { void dispose() {
_hotPlugTimer?.cancel(); _hotPlugTimer?.cancel();
_hotPlugTimer = null; _hotPlugTimer = null;
_connector.removeListener(_connectionListener); _usbConnector.removeListener(_connectionListener);
if (!_navigatedToContacts && if (!_navigatedToContacts &&
_connector.activeTransport == MeshCoreTransportType.usb && _usbConnector.activeTransport == MeshCoreTransportType.usb &&
_connector.state != MeshCoreConnectionState.disconnected) { _usbConnector.state != MeshCoreConnectionState.disconnected) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
unawaited(_connector.disconnect(manual: true)); unawaited(_usbConnector.disconnect(manual: true));
}); });
} }
super.dispose(); super.dispose();
@@ -113,6 +117,23 @@ class _UsbScreenState extends State<UsbScreen> {
style: theme.textTheme.titleLarge, style: theme.textTheme.titleLarge,
), ),
centerTitle: true, centerTitle: true,
actions: [
if (PlatformInfo.isWeb ||
PlatformInfo.isAndroid ||
PlatformInfo.isIOS)
TextButton.icon(
onPressed: () {
debugPrint(
'UsbScreen: Bluetooth selected, opening ScannerScreen',
);
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const ScannerScreen()),
);
},
icon: const Icon(Icons.bluetooth),
label: Text(l10n.connectionChoiceBluetoothLabel),
),
],
), ),
body: SafeArea( body: SafeArea(
child: LayoutBuilder( child: LayoutBuilder(
@@ -376,7 +397,8 @@ class _UsbScreenState extends State<UsbScreen> {
final isSelected = port == _selectedPort; final isSelected = port == _selectedPort;
final displayName = _friendlyPortName(port); final displayName = _friendlyPortName(port);
final rawName = normalizeUsbPortName(port); final rawName = normalizeUsbPortName(port);
final showRawName = rawName != displayName; final showRawName =
rawName != displayName && !rawName.startsWith('web:');
return Material( return Material(
color: isSelected color: isSelected
? theme.colorScheme.primaryContainer ? theme.colorScheme.primaryContainer
@@ -433,7 +455,7 @@ class _UsbScreenState extends State<UsbScreen> {
Future<void> _loadPorts() async { Future<void> _loadPorts() async {
if (!mounted) return; if (!mounted) return;
_connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus); _usbConnector.setRequestPortLabel(context.l10n.usbScreenStatus);
setState(() { setState(() {
_isLoadingPorts = true; _isLoadingPorts = true;
@@ -441,7 +463,7 @@ class _UsbScreenState extends State<UsbScreen> {
}); });
try { try {
final ports = await _connector.listUsbPorts(); final ports = await _usbConnector.listPorts();
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_ports _ports
@@ -470,8 +492,8 @@ class _UsbScreenState extends State<UsbScreen> {
if (selectedPort == null || selectedPort.isEmpty) { if (selectedPort == null || selectedPort.isEmpty) {
return; return;
} }
_connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus); _usbConnector.setRequestPortLabel(context.l10n.usbScreenStatus);
if (_connector.state != MeshCoreConnectionState.disconnected) { if (_usbConnector.state != MeshCoreConnectionState.disconnected) {
setState(() { setState(() {
_isConnecting = false; _isConnecting = false;
_errorText = null; _errorText = null;
@@ -486,7 +508,7 @@ class _UsbScreenState extends State<UsbScreen> {
}); });
try { try {
await _connector.connectUsb(portName: rawPortName); await _usbConnector.connect(portName: rawPortName);
} catch (error, stackTrace) { } catch (error, stackTrace) {
debugPrint( debugPrint(
'UsbScreen: connect failed for $rawPortName: $error\n$stackTrace', 'UsbScreen: connect failed for $rawPortName: $error\n$stackTrace',
@@ -280,6 +280,11 @@ class UsbSerialService {
Future<void> disconnect() async { Future<void> disconnect() async {
if (_status == UsbSerialStatus.disconnected) return; if (_status == UsbSerialStatus.disconnected) return;
final portLabel = _connectedPortLabel ?? _connectedPortKey;
_debugLogService?.info(
'USB disconnect starting port=${portLabel ?? 'unknown'}',
tag: 'USB Serial',
);
_status = UsbSerialStatus.disconnecting; _status = UsbSerialStatus.disconnecting;
_connectedPortKey = null; _connectedPortKey = null;
_connectedPortLabel = null; _connectedPortLabel = null;
@@ -319,6 +324,10 @@ class UsbSerialService {
_dataSubscription = null; _dataSubscription = null;
} }
_status = UsbSerialStatus.disconnected; _status = UsbSerialStatus.disconnected;
_debugLogService?.info(
'USB disconnect complete port=${portLabel ?? 'unknown'}',
tag: 'USB Serial',
);
} }
void setRequestPortLabel(String label) { void setRequestPortLabel(String label) {
+90 -20
View File
@@ -16,6 +16,10 @@ class UsbSerialService {
'2886:1667': 'Seeed Wio Tracker L1', '2886:1667': 'Seeed Wio Tracker L1',
}; };
static final Map<String, String> _deviceNamesByPortKey = <String, String>{}; 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 = final StreamController<Uint8List> _frameController =
StreamController<Uint8List>.broadcast(); StreamController<Uint8List>.broadcast();
@@ -51,11 +55,12 @@ class UsbSerialService {
return const <String>[]; return const <String>[];
} }
_resetPortCache();
final ports = await _getAuthorizedPorts(); final ports = await _getAuthorizedPorts();
if (ports.isEmpty) { if (ports.isEmpty) {
return <String>[_requestPortLabel]; return <String>[_requestPortListEntry];
} }
return ports.map(_displayLabelForPort).toList(growable: false); return ports.map(_listEntryForPort).toList(growable: false);
} }
Future<void> connect({ Future<void> connect({
@@ -75,8 +80,12 @@ class UsbSerialService {
try { try {
final requestedPortName = normalizeUsbPortName(portName); final requestedPortName = normalizeUsbPortName(portName);
final selectedPortKey = requestedPortName.startsWith('web:port:')
? requestedPortName
: null;
_port = _authorizedPortsByKey[requestedPortName];
final authorizedPorts = await _getAuthorizedPorts(); final authorizedPorts = await _getAuthorizedPorts();
_port = _selectPort(authorizedPorts, requestedPortName); _port ??= _selectPort(authorizedPorts, requestedPortName);
_port ??= await _requestPort(); _port ??= await _requestPort();
if (_port == null) { if (_port == null) {
@@ -84,8 +93,11 @@ class UsbSerialService {
} }
await _openPort(_port!, baudRate); await _openPort(_port!, baudRate);
_connectedPortKey = _portKeyFor(_port!); _connectedPortKey = _cachePort(_port!, preferredKey: selectedPortKey);
_connectedPortName = _buildDisplayLabel(_connectedPortKey!); _connectedPortName = _displayLabelForPort(
_port!,
portKey: _connectedPortKey,
);
_writer = _getWriter(_port!); _writer = _getWriter(_port!);
_reader = _getReader(_port!); _reader = _getReader(_port!);
_status = UsbSerialStatus.connected; _status = UsbSerialStatus.connected;
@@ -122,6 +134,11 @@ class UsbSerialService {
Future<void> disconnect() async { Future<void> disconnect() async {
if (_status == UsbSerialStatus.disconnected) return; if (_status == UsbSerialStatus.disconnected) return;
final portLabel = _connectedPortName ?? _connectedPortKey;
_debugLogService?.info(
'USB disconnect starting port=${portLabel ?? 'unknown'}',
tag: 'USB Serial',
);
_status = UsbSerialStatus.disconnecting; _status = UsbSerialStatus.disconnecting;
final reader = _reader; final reader = _reader;
final writer = _writer; final writer = _writer;
@@ -156,6 +173,10 @@ class UsbSerialService {
} }
_status = UsbSerialStatus.disconnected; _status = UsbSerialStatus.disconnected;
_debugLogService?.info(
'USB disconnect complete port=${portLabel ?? 'unknown'}',
tag: 'USB Serial',
);
} }
void updateConnectedLabel(String label) { void updateConnectedLabel(String label) {
@@ -210,9 +231,12 @@ class UsbSerialService {
if (ports.isEmpty) { if (ports.isEmpty) {
return null; return null;
} }
if (requestedPortName.isEmpty || requestedPortName == _requestPortLabel) { if (requestedPortName.isEmpty || requestedPortName == _requestPortKey) {
return ports.first; return ports.first;
} }
if (requestedPortName.startsWith('web:port:')) {
return null;
}
for (final port in ports) { for (final port in ports) {
final description = _describePort(port); final description = _describePort(port);
if (description == requestedPortName) { if (description == requestedPortName) {
@@ -368,10 +392,29 @@ class UsbSerialService {
} }
String _describePort(JSObject port) { 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,
knownUsbNames: _knownUsbNames,
);
}
_WebPortInfo? _portInfo(JSObject port) {
try { try {
final info = port.callMethod<JSAny?>('getInfo'.toJS); final info = port.callMethod<JSAny?>('getInfo'.toJS);
if (info == null) { if (info == null) {
return _requestPortLabel; return null;
} }
final infoObject = info as JSObject; final infoObject = info as JSObject;
@@ -381,32 +424,52 @@ class UsbSerialService {
final productId = infoObject final productId = infoObject
.getProperty<JSAny?>('usbProductId'.toJS) .getProperty<JSAny?>('usbProductId'.toJS)
?.dartify(); ?.dartify();
final hasVendor = vendorId is num; return _WebPortInfo(
final hasProduct = productId is num; usbVendorId: vendorId is num ? vendorId.toInt() : null,
usbProductId: productId is num ? productId.toInt() : null,
return describeWebUsbPort(
vendorId: hasVendor ? vendorId.toInt() : null,
productId: hasProduct ? productId.toInt() : null,
requestPortLabel: _requestPortLabel,
knownUsbNames: _knownUsbNames,
); );
} catch (_) { } catch (_) {
return _requestPortLabel; return null;
} }
} }
String _portKeyFor(JSObject port) => _describePort(port); String _portKeyFor(JSObject port) {
return _cachePort(port);
}
String _displayLabelForPort(JSObject port) => String _cachePort(JSObject port, {String? preferredKey}) {
_buildDisplayLabel(_portKeyFor(port)); 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) { String _buildDisplayLabel(String portKey) {
return buildUsbDisplayLabel( return buildUsbDisplayLabel(
basePortLabel: portKey, basePortLabel: _baseLabelsByPortKey[portKey] ?? portKey,
deviceName: _deviceNamesByPortKey[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) { void _releaseLock(JSObject resource) {
try { try {
resource.callMethod<JSAny?>('releaseLock'.toJS); resource.callMethod<JSAny?>('releaseLock'.toJS);
@@ -462,3 +525,10 @@ class UsbSerialService {
} }
enum UsbSerialStatus { disconnected, connecting, connected, disconnecting } enum UsbSerialStatus { disconnected, connecting, connected, disconnecting }
final class _WebPortInfo {
const _WebPortInfo({required this.usbVendorId, required this.usbProductId});
final int? usbVendorId;
final int? usbProductId;
}
+2
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../connector/meshcore_connector.dart'; import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import 'app_logger.dart';
/// Shows a confirmation dialog before disconnecting from the device. /// Shows a confirmation dialog before disconnecting from the device.
/// Returns true if user confirmed and disconnect completed, false otherwise. /// Returns true if user confirmed and disconnect completed, false otherwise.
@@ -28,6 +29,7 @@ Future<bool> showDisconnectDialog(
); );
if (confirmed == true) { if (confirmed == true) {
appLogger.info('Disconnect confirmed from popup', tag: 'Connection');
await connector.disconnect(); await connector.disconnect();
return true; return true;
} }
+5 -9
View File
@@ -4,7 +4,7 @@ import 'package:provider/provider.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart'; import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/l10n/app_localizations.dart'; import 'package:meshcore_open/l10n/app_localizations.dart';
import 'package:meshcore_open/screens/connection_choice_screen.dart'; import 'package:meshcore_open/screens/scanner_screen.dart';
import 'package:meshcore_open/screens/usb_screen.dart'; import 'package:meshcore_open/screens/usb_screen.dart';
import 'package:meshcore_open/utils/platform_info.dart'; import 'package:meshcore_open/utils/platform_info.dart';
@@ -131,7 +131,7 @@ void main() {
}, },
); );
testWidgets('ConnectionChoiceScreen USB button reflects platform support', ( testWidgets('ScannerScreen USB action reflects platform support', (
tester, tester,
) async { ) async {
final connector = _FakeMeshCoreConnector(); final connector = _FakeMeshCoreConnector();
@@ -139,19 +139,15 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
_buildTestApp( _buildTestApp(
connector: connector, connector: connector,
child: const ConnectionChoiceScreen(), child: const ScannerScreen(),
), ),
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final usbButton = tester.widget<ElevatedButton>(
find.widgetWithText(ElevatedButton, 'USB'),
);
if (PlatformInfo.supportsUsbSerial) { if (PlatformInfo.supportsUsbSerial) {
expect(usbButton.onPressed, isNotNull); expect(find.widgetWithText(FloatingActionButton, 'USB'), findsOneWidget);
} else { } else {
expect(usbButton.onPressed, isNull); expect(find.widgetWithText(FloatingActionButton, 'USB'), findsNothing);
} }
}); });
} }