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

View file

@ -16,7 +16,7 @@ if (keystorePropertiesFile.exists()) {
android {
namespace = "com.meshcore.meshcore_open"
compileSdk = flutter.compileSdkVersion
ndkVersion = "29.0.14206865"
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
@ -84,5 +84,4 @@ flutter {
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
implementation("com.github.mik3y:usb-serial-for-android:3.9.0")
}

View file

@ -1,408 +1,18 @@
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.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() {
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 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)
}
}
private val usbFunctions by lazy { MeshcoreUsbFunctions(this) }
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.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
}
},
)
usbFunctions.configureFlutterEngine(flutterEngine)
}
override fun onDestroy() {
closeUsbConnection()
usbIoExecutor.shutdownNow()
try {
unregisterReceiver(permissionReceiver)
} catch (_: IllegalArgumentException) {
}
usbFunctions.dispose()
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
}
}

View file

@ -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
}
}

View file

@ -2,7 +2,6 @@ allprojects {
repositories {
google()
mavenCentral()
maven(url = "https://jitpack.io")
}
}

View file

@ -166,6 +166,9 @@ class MeshCoreConnector extends ChangeNotifier {
bool _hasReceivedDeviceInfo = false;
bool _pendingInitialChannelSync = false;
bool _pendingInitialContactsSync = false;
bool _bleInitialSyncStarted = false;
bool _pendingDeferredChannelSyncAfterContacts = false;
bool _webInitialHandshakeRequestSent = false;
bool _preserveContactsOnRefresh = false;
static const int _defaultMaxContacts = 32;
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));
final windowedMergedMessages = mergedMessages.length > _messageWindowSize
? mergedMessages.sublist(mergedMessages.length - _messageWindowSize)
@ -820,6 +825,76 @@ class MeshCoreConnector extends ChangeNotifier {
_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 {
if (_state == MeshCoreConnectionState.connecting ||
_state == MeshCoreConnectionState.connected) {
@ -844,6 +919,7 @@ class MeshCoreConnector extends ChangeNotifier {
_lastDeviceDisplayName = _deviceDisplayName;
_manualDisconnect = false;
_cancelReconnectTimer();
_bleInitialSyncStarted = false;
if (PlatformInfo.isWeb) {
_resetConnectionHandshakeState();
}
@ -856,6 +932,10 @@ class MeshCoreConnector extends ChangeNotifier {
'Starting connect to $connectLabel',
tag: 'BLE Connect',
);
await _connectionSubscription?.cancel();
_connectionSubscription = null;
await _notifySubscription?.cancel();
_notifySubscription = null;
_connectionSubscription = device.connectionState.listen((state) {
if (state == BluetoothConnectionState.disconnected && isConnected) {
_handleDisconnection();
@ -899,6 +979,8 @@ class MeshCoreConnector extends ChangeNotifier {
);
if (PlatformInfo.isWeb &&
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(
'retrying service discovery after transient web disconnect',
tag: 'BLE Connect',
@ -995,42 +1077,7 @@ class MeshCoreConnector extends ChangeNotifier {
_hasReceivedDeviceInfo = false;
_pendingInitialChannelSync = true;
}
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());
}
unawaited(Future<void>.microtask(() => _startBleInitialSync()));
} catch (e) {
_appDebugLogService?.error('Connection error: $e', tag: 'BLE Connect');
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 {
if (_selfPublicKey != null) return true;
if (!isConnected) return false;
@ -1139,17 +1116,60 @@ class MeshCoreConnector extends ChangeNotifier {
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() {
_selfPublicKey = null;
_selfName = null;
_selfLatitude = null;
_selfLongitude = null;
_awaitingSelfInfo = false;
_webInitialHandshakeRequestSent = false;
_selfInfoRetryTimer?.cancel();
_selfInfoRetryTimer = null;
_hasReceivedDeviceInfo = false;
_pendingInitialChannelSync = false;
_pendingInitialContactsSync = false;
_bleInitialSyncStarted = false;
_pendingDeferredChannelSyncAfterContacts = false;
_webInitialHandshakeRequestSent = false;
}
bool get _shouldAutoReconnect =>
@ -1205,6 +1225,14 @@ class MeshCoreConnector extends ChangeNotifier {
Future<void> disconnect({bool manual = true}) async {
if (_state == MeshCoreConnectionState.disconnecting) return;
final transportAtDisconnect = _activeTransport;
final transportLabel = transportAtDisconnect == MeshCoreTransportType.usb
? 'USB'
: 'BLE';
_appDebugLogService?.info(
'Starting disconnect transport=$transportLabel manual=$manual',
tag: 'Connection',
);
if (manual) {
_manualDisconnect = true;
@ -1280,6 +1308,10 @@ class MeshCoreConnector extends ChangeNotifier {
_activeUsbPortLabel = null;
_setState(MeshCoreConnectionState.disconnected);
_appDebugLogService?.info(
'Disconnect complete transport=$transportLabel manual=$manual',
tag: 'Connection',
);
if (!manual && transportAtDisconnect == MeshCoreTransportType.bluetooth) {
_scheduleReconnect();
}
@ -1345,7 +1377,18 @@ class MeshCoreConnector extends ChangeNotifier {
Future<void> refreshDeviceInfo() async {
if (!isConnected) return;
if (PlatformInfo.isWeb &&
_activeTransport == MeshCoreTransportType.bluetooth &&
_webInitialHandshakeRequestSent &&
_selfPublicKey == null) {
return;
}
_awaitingSelfInfo = true;
if (PlatformInfo.isWeb &&
_activeTransport == MeshCoreTransportType.bluetooth &&
_selfPublicKey == null) {
_webInitialHandshakeRequestSent = true;
}
await sendFrame(buildDeviceQueryFrame());
await sendFrame(buildAppStartFrame());
await requestBatteryStatus(force: true);
@ -1356,7 +1399,18 @@ class MeshCoreConnector extends ChangeNotifier {
Future<void> _requestDeviceInfo() async {
if (!isConnected || _awaitingSelfInfo) return;
if (PlatformInfo.isWeb &&
_activeTransport == MeshCoreTransportType.bluetooth &&
_webInitialHandshakeRequestSent &&
_selfPublicKey == null) {
return;
}
_awaitingSelfInfo = true;
if (PlatformInfo.isWeb &&
_activeTransport == MeshCoreTransportType.bluetooth &&
_selfPublicKey == null) {
_webInitialHandshakeRequestSent = true;
}
await sendFrame(buildDeviceQueryFrame());
await sendFrame(buildAppStartFrame());
await sendFrame(buildGetCustomVarsFrame());
@ -2183,6 +2237,12 @@ class MeshCoreConnector extends ChangeNotifier {
_pendingQueueSync = false;
unawaited(syncQueuedMessages(force: true));
}
if (_pendingDeferredChannelSyncAfterContacts &&
(_activeTransport == MeshCoreTransportType.bluetooth ||
_activeTransport == MeshCoreTransportType.usb)) {
_pendingDeferredChannelSyncAfterContacts = false;
unawaited(getChannels());
}
break;
case respCodeContactMsgRecv:
case respCodeContactMsgRecvV3:
@ -2294,6 +2354,8 @@ class MeshCoreConnector extends ChangeNotifier {
// [58+] = node_name
if (frame.length < 4 + pubKeySize) return;
final wasAwaitingSelfInfo = _awaitingSelfInfo;
_currentTxPower = frame[2];
_maxTxPower = frame[3];
_selfPublicKey = Uint8List.fromList(frame.sublist(4, 4 + pubKeySize));
@ -2325,15 +2387,25 @@ class MeshCoreConnector extends ChangeNotifier {
_selfInfoRetryTimer = null;
notifyListeners();
if (PlatformInfo.isWeb &&
_activeTransport == MeshCoreTransportType.bluetooth &&
!wasAwaitingSelfInfo) {
return;
}
// Auto-fetch contacts after getting self info. On web BLE, defer this
// until after channel 0 so startup writes stay serialized.
if (PlatformInfo.isWeb &&
_activeTransport == MeshCoreTransportType.bluetooth) {
_pendingInitialContactsSync = true;
} else if (_activeTransport == MeshCoreTransportType.usb) {
_pendingDeferredChannelSyncAfterContacts = true;
getContacts();
} else {
getContacts();
}
if (_shouldGateInitialChannelSync) {
if (_shouldGateInitialChannelSync &&
_activeTransport != MeshCoreTransportType.usb) {
_maybeStartInitialChannelSync();
}
}
@ -2367,6 +2439,7 @@ class MeshCoreConnector extends ChangeNotifier {
unawaited(loadChannelSettings(maxChannels: nextMaxChannels));
unawaited(loadAllChannelMessages(maxChannels: nextMaxChannels));
if (isConnected &&
_selfPublicKey != null &&
(!_shouldGateInitialChannelSync || !_pendingInitialChannelSync)) {
unawaited(getChannels(maxChannels: nextMaxChannels));
}
@ -3524,17 +3597,13 @@ class MeshCoreConnector extends ChangeNotifier {
// For 1:1 chats, sender is implicit (null)
String? senderName;
if (isRoomServer && !msg.isOutgoing) {
// Treat a missing room-contact key as unknown instead of matching every
// contact via an empty prefix.
if (msg.fourByteRoomContactKey.length == 4) {
final senderContact = _contacts.cast<Contact?>().firstWhere(
(c) =>
c != null &&
_matchesPrefix(c.publicKey, msg.fourByteRoomContactKey),
orElse: () => null,
);
senderName = senderContact?.name;
}
final senderContact = _contacts.cast<Contact?>().firstWhere(
(c) =>
c != null &&
_matchesPrefix(c.publicKey, msg.fourByteRoomContactKey),
orElse: () => null,
);
senderName = senderContact?.name;
} else if (isRoomServer && msg.isOutgoing) {
senderName = selfName;
}

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);
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
{
{
"@@locale": "en",
"appTitle": "MeshCore Open",
"nav_contacts": "Contacts",
@ -28,7 +28,7 @@
"common_disable": "Disable",
"common_reboot": "Reboot",
"common_loading": "Loading...",
"common_notAvailable": "",
"common_notAvailable": "—",
"common_voltageValue": "{volts} V",
"@common_voltageValue": {
"placeholders": {
@ -46,8 +46,6 @@
}
},
"scanner_title": "MeshCore Open",
"connectionChoiceTitle": "Choose your connection method",
"connectionChoiceSubtitle": "Select how you would like to reach your MeshCore device.",
"connectionChoiceUsbLabel": "USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
"usbScreenTitle": "Connect over USB",
@ -180,20 +178,20 @@
"appSettings_language": "Language",
"appSettings_languageSystem": "System default",
"appSettings_languageEn": "English",
"appSettings_languageFr": "Français",
"appSettings_languageEs": "Español",
"appSettings_languageFr": "Français",
"appSettings_languageEs": "Español",
"appSettings_languageDe": "Deutsch",
"appSettings_languagePl": "Polski",
"appSettings_languageSl": "Slovenščina",
"appSettings_languagePt": "Português",
"appSettings_languageSl": "Slovenščina",
"appSettings_languagePt": "Português",
"appSettings_languageIt": "Italiano",
"appSettings_languageZh": "中文",
"appSettings_languageZh": "中文",
"appSettings_languageSv": "Svenska",
"appSettings_languageNl": "Nederlands",
"appSettings_languageSk": "Slovenčina",
"appSettings_languageBg": "Български",
"appSettings_languageRu": "Русский",
"appSettings_languageUk": "Українська",
"appSettings_languageSk": "Slovenčina",
"appSettings_languageBg": "Български",
"appSettings_languageRu": "Русский",
"appSettings_languageUk": "Українська",
"appSettings_enableMessageTracing": "Enable Message Tracing",
"appSettings_enableMessageTracingSubtitle": "Show detailed routing and timing metadata for messages",
"appSettings_notifications": "Notifications",
@ -1341,7 +1339,7 @@
}
}
},
"telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F",
"telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F",
"@telemetry_temperatureValue": {
"placeholders": {
"celsius": {
@ -1391,7 +1389,7 @@
"channelPath_repeatsLabel": "Repeats",
"channelPath_pathLabel": "Path {index}",
"channelPath_observedLabel": "Observed",
"channelPath_observedPathTitle": "Observed path {index} {hops}",
"channelPath_observedPathTitle": "Observed path {index} • {hops}",
"@channelPath_observedPathTitle": {
"placeholders": {
"index": {
@ -1466,7 +1464,7 @@
},
"channelPath_pathLabelTitle": "Path",
"channelPath_observedPathHeader": "Observed Path",
"channelPath_selectedPathLabel": "{label} {prefixes}",
"channelPath_selectedPathLabel": "{label} • {prefixes}",
"@channelPath_selectedPathLabel": {
"placeholders": {
"label": {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
{
{
"channels_channelDeleteFailed": "Impossibile eliminare il canale \"{name}\"",
"@channels_channelDeleteFailed": {
"placeholders": {
@ -35,7 +35,7 @@
"common_disable": "Disattivare",
"common_reboot": "Riavvia",
"common_loading": "Caricamento...",
"common_notAvailable": "",
"common_notAvailable": "—",
"common_voltageValue": "{volts} V",
"@common_voltageValue": {
"placeholders": {
@ -98,11 +98,11 @@
"settings_locationInvalid": "Latitudine o longitudine non valida.",
"settings_latitude": "Latitudine",
"settings_longitude": "Longitudine",
"settings_privacyMode": "Modalità Privacy",
"settings_privacyMode": "Modalità Privacy",
"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_privacyModeEnabled": "Modalità privacy abilitata",
"settings_privacyModeDisabled": "Modalità privacy disabilitata",
"settings_privacyModeToggle": "Attiva la modalità privacy per nascondere il tuo nome e la tua posizione negli annunci.",
"settings_privacyModeEnabled": "Modalità privacy abilitata",
"settings_privacyModeDisabled": "Modalità privacy disabilitata",
"settings_actions": "Azioni",
"settings_sendAdvertisement": "Invia Annuncio",
"settings_sendAdvertisementSubtitle": "Presenza trasmessa ora",
@ -165,18 +165,18 @@
"appSettings_language": "Lingua",
"appSettings_languageSystem": "Predefinito di sistema",
"appSettings_languageEn": "English",
"appSettings_languageFr": "Français",
"appSettings_languageEs": "Español",
"appSettings_languageFr": "Français",
"appSettings_languageEs": "Español",
"appSettings_languageDe": "Deutsch",
"appSettings_languagePl": "Polski",
"appSettings_languageSl": "Slovenščina",
"appSettings_languagePt": "Português",
"appSettings_languageSl": "Slovenščina",
"appSettings_languagePt": "Português",
"appSettings_languageIt": "Italiano",
"appSettings_languageZh": "中文",
"appSettings_languageZh": "中文",
"appSettings_languageSv": "Svenska",
"appSettings_languageNl": "Nederlands",
"appSettings_languageSk": "Slovenčina",
"appSettings_languageBg": "Български",
"appSettings_languageSk": "Slovenčina",
"appSettings_languageBg": "Български",
"appSettings_notifications": "Notifiche",
"appSettings_enableNotifications": "Abilita Notifiche",
"appSettings_enableNotificationsSubtitle": "Ricevi notifiche per messaggi e annunci",
@ -195,7 +195,7 @@
"appSettings_pathsWillBeCleared": "I percorsi verranno puliti dopo 5 tentativi falliti.",
"appSettings_pathsWillNotBeCleared": "I percorsi non verranno eliminati automaticamente.",
"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_autoRouteRotationDisabled": "Rotazione del percorso automatico disabilitata",
"appSettings_battery": "Batteria",
@ -284,8 +284,8 @@
},
"contacts_newGroup": "Nuovo Gruppo",
"contacts_groupName": "Nome gruppo",
"contacts_groupNameRequired": "Il nome del gruppo è obbligatorio.",
"contacts_groupAlreadyExists": "Il gruppo \"{name}\" esiste già.",
"contacts_groupNameRequired": "Il nome del gruppo è obbligatorio.",
"contacts_groupAlreadyExists": "Il gruppo \"{name}\" esiste già.",
"@contacts_groupAlreadyExists": {
"placeholders": {
"name": {
@ -345,7 +345,7 @@
"channels_muteChannel": "Silenzia canale",
"channels_unmuteChannel": "Attiva notifiche canale",
"channels_deleteChannel": "Elimina canale",
"channels_deleteChannelConfirm": "Eliminare \"{name}\"? Non può essere annullato.",
"channels_deleteChannelConfirm": "Eliminare \"{name}\"? Non può essere annullato.",
"@channels_deleteChannelConfirm": {
"placeholders": {
"name": {
@ -477,7 +477,7 @@
"debugLog_enableInSettings": "Abilita il logging di debug dell'app nelle impostazioni",
"debugLog_frames": "Frame",
"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": {
"placeholders": {
@ -542,11 +542,11 @@
},
"debugFrame_hexDump": "Dumpa Esadecimale:",
"chat_pathManagement": "Gestione Percorsi",
"chat_routingMode": "Modalità di routing",
"chat_routingMode": "Modalità di routing",
"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_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_hopPlural": "salta",
"chat_hopsCount": "{count} {count, plural, =1{salto} other{salti}}",
@ -559,15 +559,15 @@
},
"chat_successes": "successi",
"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_setCustomPath": "Imposta Percorso Personalizzato",
"chat_setCustomPathSubtitle": "Specifica manualmente il percorso di routing",
"chat_clearPath": "Cancella Percorso",
"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_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_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}",
@ -660,7 +660,7 @@
"map_sendToChannel": "Invia al canale",
"map_noChannelsAvailable": "Nessun canale disponibile",
"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": {
"placeholders": {
"channelLabel": {
@ -810,13 +810,13 @@
"login_password": "Password",
"login_enterPassword": "Inserisci 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_roomDescription": "Inserisci la password della stanza per accedere alle impostazioni e allo stato.",
"login_routing": "Instradamento",
"login_routingMode": "Modalità di routing",
"login_routingMode": "Modalità di routing",
"login_autoUseSavedPath": "Utilizza il percorso salvato",
"login_forceFloodMode": "Modalità Inondamento Forzato",
"login_forceFloodMode": "Modalità Inondamento Forzato",
"login_managePaths": "Gestisci Percorsi",
"login_login": "Accedi",
"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_clear": "Cancella",
"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_hexPrefixExample": "Esempio: A1,F2,3C (ogni nodo utilizza il primo byte della sua chiave pubblica)",
"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_noRepeatersFound": "Non sono stati trovati ripetitori o server di stanza.",
"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",
"repeater_management": "Gestione Ripetitori",
"repeater_managementTools": "Strumenti di Gestione",
@ -887,9 +887,9 @@
"repeater_settings": "Impostazioni",
"repeater_settingsSubtitle": "Configura i parametri del ripetitore",
"repeater_statusTitle": "Stato del Ripetitore",
"repeater_routingMode": "Modalità di routing",
"repeater_routingMode": "Modalità di routing",
"repeater_autoUseSavedPath": "Percorso salvato automatico",
"repeater_forceFloodMode": "Modalità Inondamento Forzato",
"repeater_forceFloodMode": "Modalità Inondamento Forzato",
"repeater_pathManagement": "Gestione dei percorsi",
"repeater_refresh": "Aggiorna",
"repeater_statusRequestTimeout": "Richiesta stato scaduta.",
@ -904,7 +904,7 @@
"repeater_systemInformation": "Informazioni di sistema",
"repeater_battery": "Batteria",
"repeater_clockAtLogin": "Orologio (all'accesso)",
"repeater_uptime": "Disponibilità",
"repeater_uptime": "Disponibilità",
"repeater_queueLength": "Lunghezza della coda",
"repeater_debugFlags": "Impostazioni Debug",
"repeater_radioStatistics": "Statistiche Radio",
@ -1007,10 +1007,10 @@
"repeater_packetForwardingSubtitle": "Abilita il ripetitore per inoltrare i pacchetti",
"repeater_guestAccess": "Accesso Ospite",
"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_advertisementSettings": "Impostazioni Annuncio",
"repeater_localAdvertInterval": "Intervallo Pubblicità Locale",
"repeater_localAdvertInterval": "Intervallo Pubblicità Locale",
"repeater_localAdvertIntervalMinutes": "{minutes} minuti",
"@repeater_localAdvertIntervalMinutes": {
"placeholders": {
@ -1019,7 +1019,7 @@
}
}
},
"repeater_floodAdvertInterval": "Intervallo Pubblicità Inondazione",
"repeater_floodAdvertInterval": "Intervallo Pubblicità Inondazione",
"repeater_floodAdvertIntervalHours": "{hours} ore",
"@repeater_floodAdvertIntervalHours": {
"placeholders": {
@ -1033,13 +1033,13 @@
"repeater_rebootRepeater": "Riavvia Ripetitore",
"repeater_rebootRepeaterSubtitle": "Riavvia il dispositivo 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_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_eraseFileSystemSubtitle": "Formatta il file system del ripetitore",
"repeater_eraseFileSystemConfirm": "ATTENZIONE: Ciò cancellerà tutti i dati sul ripetitore. Non può essere annullato!",
"repeater_eraseSerialOnly": "Elimina è disponibile solo tramite console seriale.",
"repeater_eraseFileSystemConfirm": "ATTENZIONE: Ciò cancellerà tutti i dati sul ripetitore. Non può essere annullato!",
"repeater_eraseSerialOnly": "Elimina è disponibile solo tramite console seriale.",
"repeater_commandSent": "Comando inviato: {command}",
"@repeater_commandSent": {
"placeholders": {
@ -1072,7 +1072,7 @@
"repeater_refreshLocationSettings": "Aggiorna le Impostazioni della Posizione",
"repeater_refreshPacketForwarding": "Aggiorna il inoltro pacchetti",
"repeater_refreshGuestAccess": "Aggiorna Accesso Ospite",
"repeater_refreshPrivacyMode": "Aggiorna Modalità Privacy",
"repeater_refreshPrivacyMode": "Aggiorna Modalità Privacy",
"repeater_refreshAdvertisementSettings": "Aggiorna le Impostazioni dell'Annuncio",
"repeater_refreshed": "{label} aggiornato",
"@repeater_refreshed": {
@ -1117,7 +1117,7 @@
"repeater_cliQuickAdvertise": "Pubblicare",
"repeater_cliQuickClock": "Orologio",
"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_cliHelpPassword": "Imposta una nuova password di amministratore per il dispositivo.",
"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_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_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_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_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_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.",
@ -1138,33 +1138,33 @@
"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_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_cliHelpSetDirectTxDelay": "Uguale a txdelay, ma per applicare un ritardo casuale alla inoltrata di pacchetti in modalità diretta.",
"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_cliHelpSetBridgeEnabled": "Abilita/Disabilita ponte.",
"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_cliHelpSetBridgeBaud": "Imposta la velocità di trasmissione per i ponti rs232.",
"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_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_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_cliHelpLogStart": "Avvia registrazione pacchetti nel file system.",
"repeater_cliHelpLogStop": "Interrompi la registrazione dei pacchetti al 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_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_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_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_cliHelpRegionHomeSet": "Imposta la regione 'home'.",
"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_cliHelpGpsSync": "Sincronizza l'orario del nodo con l'orologio GPS.",
"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_regionNote": "Sono state introdotte le comandi di regione per gestire le definizioni e le autorizzazioni delle regioni.",
"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_requestTimeout": "Richiesta di telemetria scaduta.",
"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": {
"placeholders": {
"celsius": {
@ -1254,7 +1254,7 @@
"channelPath_repeatsLabel": "Ripeti",
"channelPath_pathLabel": "Percorso {index}",
"channelPath_observedLabel": "Osservato",
"channelPath_observedPathTitle": "Percorso osservato {index} {hops}",
"channelPath_observedPathTitle": "Percorso osservato {index} • {hops}",
"@channelPath_observedPathTitle": {
"placeholders": {
"index": {
@ -1329,7 +1329,7 @@
},
"channelPath_pathLabelTitle": "Percorso",
"channelPath_observedPathHeader": "Percorso Osservato",
"channelPath_selectedPathLabel": "{label} {prefixes}",
"channelPath_selectedPathLabel": "{label} • {prefixes}",
"@channelPath_selectedPathLabel": {
"placeholders": {
"label": {
@ -1373,11 +1373,11 @@
"channels_joinPrivateChannel": "Unisciti a un Canale Privato",
"channels_joinPrivateChannelDesc": "Inserire manualmente una chiave segreta.",
"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_joinHashtagChannelDesc": "Chiunque può unirsi ai canali hashtag.",
"channels_joinHashtagChannelDesc": "Chiunque può unirsi ai canali hashtag.",
"channels_scanQrCode": "Scansiona un codice QR",
"channels_scanQrCodeComingSoon": "Arriverà presto",
"channels_scanQrCodeComingSoon": "Arriverà presto",
"channels_enterHashtag": "Inserisci hashtag",
"channels_hashtagHint": "es. #team",
"@neighbors_unknownContact": {
@ -1459,35 +1459,35 @@
}
},
"common_ok": "OK",
"community_title": "Comunità",
"community_create": "Crea Comunità",
"community_createDesc": "Crea una nuova comunità e condividila tramite codice QR.",
"community_title": "Comunità",
"community_create": "Crea Comunità",
"community_createDesc": "Crea una nuova comunità e condividila tramite codice QR.",
"community_join": "Unisciti",
"community_joinTitle": "Unisciti alla Community",
"community_joinConfirmation": "Vuoi unirti alla community \"{name}\"?",
"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_publicChannel": "Comunità Pubblica",
"community_hashtagChannel": "Hashtag della Comunità",
"community_name": "Nome della Comunità",
"community_enterName": "Inserisci il nome della comunità",
"community_created": "Comunità \"{name}\" creata",
"community_joined": "Unito alla comunità \"{name}\"",
"community_qrTitle": "Condividi Comunità",
"community_publicChannel": "Comunità Pubblica",
"community_hashtagChannel": "Hashtag della Comunità",
"community_name": "Nome della Comunità",
"community_enterName": "Inserisci il nome della comunità",
"community_created": "Comunità \"{name}\" creata",
"community_joined": "Unito alla comunità \"{name}\"",
"community_qrTitle": "Condividi Comunità",
"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_invalidQrCode": "Codice QR della community non valido",
"community_alreadyMember": "Già membro",
"community_alreadyMemberMessage": "Sei già un membro di \"{name}\".",
"community_addPublicChannel": "Aggiungi Canale Pubblico della Comunità",
"community_alreadyMember": "Già membro",
"community_alreadyMemberMessage": "Sei già un membro di \"{name}\".",
"community_addPublicChannel": "Aggiungi Canale Pubblico della Comunità",
"community_addPublicChannelHint": "Aggiungi automaticamente il canale pubblico per questa community",
"community_noCommunities": "Nessun gruppo aggiunto finora",
"community_scanOrCreate": "Scansiona un codice QR o crea una community per iniziare.",
"community_manageCommunities": "Gestisci Comunità",
"community_delete": "Lascia la Comunità",
"community_manageCommunities": "Gestisci Comunità",
"community_delete": "Lascia la Comunità",
"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": {
"placeholders": {
"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_addHashtagChannelDesc": "Aggiungi un canale con hashtag per questa community",
"community_selectCommunity": "Seleziona Comunità",
"community_selectCommunity": "Seleziona Comunità",
"community_regularHashtag": "Hashtag regolare",
"community_regularHashtagDesc": "Hashtag pubblico (chiunque può unirsi)",
"community_communityHashtag": "Hashtag della Comunità",
"community_communityHashtagDesc": "Visibile solo ai membri della comunità",
"community_regularHashtagDesc": "Hashtag pubblico (chiunque può unirsi)",
"community_communityHashtag": "Hashtag della Comunità",
"community_communityHashtagDesc": "Visibile solo ai membri della comunità",
"community_forCommunity": "Per {name}",
"@community_regenerateSecretConfirm": {
"placeholders": {
@ -1567,16 +1567,16 @@
"contacts_floodAdvert": "Annuncio alluvionale",
"contacts_copyAdvertToClipboard": "Copia Annuncio negli Appunti",
"contacts_addContactFromClipboard": "Aggiungere contatto dalla clipboard",
"contacts_clipboardEmpty": "La clipboard è vuota.",
"contacts_clipboardEmpty": "La clipboard è vuota.",
"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_zeroHopContactAdvertSent": "Inviato contatto tramite annuncio.",
"contacts_contactAdvertCopyFailed": "Copia dell'annuncio nella Clipboard non riuscita.",
"contacts_ShareContactZeroHop": "Condividi contatto tramite annuncio",
"contacts_zeroHopContactAdvertFailed": "Invio del contatto non riuscito.",
"contacts_contactAdvertCopied": "Annuncio copiato negli Appunti.",
"notification_activityTitle": "Attività MeshCore",
"notification_activityTitle": "Attività MeshCore",
"notification_messagesCount": "{count} {count, plural, =1{messaggio} other{messaggi}}",
"notification_channelMessagesCount": "{count} {count, plural, =1{messaggio del canale} other{messaggi del canale}}",
"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_gpxExportNoContacts": "Nessun contatto da esportare.",
"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_gpxExportContactsSubtitle": "Esporta i compagni con una posizione in un file GPX.",
"settings_gpxExportAll": "Esporta tutti i contatti in GPX",
@ -1597,13 +1597,13 @@
"settings_gpxExportAllContacts": "Tutte le posizioni dei contatti",
"settings_gpxExportShareText": "Dati mappa esportati da meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open esportazione dati mappa GPX",
"pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!",
"pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!",
"map_removeLast": "Rimuovi ultimo",
"map_pathTraceCancelled": "Tracciamento del percorso annullato.",
"pathTrace_clearTooltip": "Pulisci percorso",
"map_runTrace": "Esegui Path Trace",
"map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.",
"scanner_bluetoothOff": "Il Bluetooth è disattivato.",
"scanner_bluetoothOff": "Il Bluetooth è disattivato.",
"scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.",
"scanner_chromeRequired": "Browser Chrome richiesto",
"scanner_chromeRequiredMessage": "Questa applicazione web richiede Google Chrome o un browser basato su Chromium per il supporto Bluetooth.",
@ -1612,10 +1612,10 @@
"snrIndicator_lastSeen": "Ultimo accesso",
"chat_ShowAllPaths": "Mostra tutti i percorsi",
"settings_clientRepeat": "Ripetizione \"fuori dalla rete\"",
"settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.",
"settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.",
"settings_clientRepeatSubtitle": "Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri.",
"settings_aboutOpenMeteoAttribution": "Dati di elevazione LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Unità",
"appSettings_unitsTitle": "Unità",
"appSettings_unitsMetric": "Metrico (m/km)",
"appSettings_unitsImperial": "Imperiale (ft / mi)",
"map_lineOfSight": "Linea di vista",
@ -1631,7 +1631,7 @@
},
"losClearAllPoints": "Cancella tutti i punti",
"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",
"losShowDisplayNodes": "Mostra i nodi di visualizzazione",
"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.",
"losRenameCustomPoint": "Rinomina punto personalizzato",
"losPointName": "Nome del punto",
@ -1734,7 +1734,7 @@
"losLegendTerrain": "Terreno",
"losFrequencyLabel": "Frequenza",
"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": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
@ -1802,11 +1802,9 @@
"contacts_unread": "Non letti",
"contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...",
"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",
"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",
"usbScreenSubtitle": "Seleziona il dispositivo seriale rilevato e connettilo direttamente al tuo nodo MeshCore.",
"usbScreenTitle": "Connessione tramite USB",

View file

@ -295,7 +295,7 @@ abstract class AppLocalizations {
/// No description provided for @common_notAvailable.
///
/// In en, this message translates to:
/// **''**
/// **'—'**
String get common_notAvailable;
/// No description provided for @common_voltageValue.
@ -316,18 +316,6 @@ abstract class AppLocalizations {
/// **'MeshCore Open'**
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.
///
/// In en, this message translates to:
@ -955,13 +943,13 @@ abstract class AppLocalizations {
/// No description provided for @appSettings_languageFr.
///
/// In en, this message translates to:
/// **'Français'**
/// **'Français'**
String get appSettings_languageFr;
/// No description provided for @appSettings_languageEs.
///
/// In en, this message translates to:
/// **'Español'**
/// **'Español'**
String get appSettings_languageEs;
/// No description provided for @appSettings_languageDe.
@ -979,13 +967,13 @@ abstract class AppLocalizations {
/// No description provided for @appSettings_languageSl.
///
/// In en, this message translates to:
/// **'Slovenščina'**
/// **'Slovenščina'**
String get appSettings_languageSl;
/// No description provided for @appSettings_languagePt.
///
/// In en, this message translates to:
/// **'Português'**
/// **'Português'**
String get appSettings_languagePt;
/// No description provided for @appSettings_languageIt.
@ -997,7 +985,7 @@ abstract class AppLocalizations {
/// No description provided for @appSettings_languageZh.
///
/// In en, this message translates to:
/// **'中文'**
/// **'中文'**
String get appSettings_languageZh;
/// No description provided for @appSettings_languageSv.
@ -1015,25 +1003,25 @@ abstract class AppLocalizations {
/// No description provided for @appSettings_languageSk.
///
/// In en, this message translates to:
/// **'Slovenčina'**
/// **'Slovenčina'**
String get appSettings_languageSk;
/// No description provided for @appSettings_languageBg.
///
/// In en, this message translates to:
/// **'Български'**
/// **'Български'**
String get appSettings_languageBg;
/// No description provided for @appSettings_languageRu.
///
/// In en, this message translates to:
/// **'Русский'**
/// **'Русский'**
String get appSettings_languageRu;
/// No description provided for @appSettings_languageUk.
///
/// In en, this message translates to:
/// **'Українська'**
/// **'Українська'**
String get appSettings_languageUk;
/// No description provided for @appSettings_enableMessageTracing.
@ -4349,7 +4337,7 @@ abstract class AppLocalizations {
/// No description provided for @telemetry_temperatureValue.
///
/// In en, this message translates to:
/// **'{celsius}°C / {fahrenheit}°F'**
/// **'{celsius}°C / {fahrenheit}°F'**
String telemetry_temperatureValue(String celsius, String fahrenheit);
/// No description provided for @neighbors_receivedData.
@ -4463,7 +4451,7 @@ abstract class AppLocalizations {
/// No description provided for @channelPath_observedPathTitle.
///
/// In en, this message translates to:
/// **'Observed path {index} {hops}'**
/// **'Observed path {index} • {hops}'**
String channelPath_observedPathTitle(int index, String hops);
/// No description provided for @channelPath_noLocationData.
@ -4547,7 +4535,7 @@ abstract class AppLocalizations {
/// No description provided for @channelPath_selectedPathLabel.
///
/// In en, this message translates to:
/// **'{label} {prefixes}'**
/// **'{label} • {prefixes}'**
String channelPath_selectedPathLabel(String label, String prefixes);
/// 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

View file

@ -93,7 +93,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get common_loading => 'Loading...';
@override
String get common_notAvailable => '';
String get common_notAvailable => '—';
@override
String common_voltageValue(String volts) {
@ -108,13 +108,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
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
String get connectionChoiceUsbLabel => 'USB';
@ -455,10 +448,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get appSettings_languageEn => 'English';
@override
String get appSettings_languageFr => 'Français';
String get appSettings_languageFr => 'Français';
@override
String get appSettings_languageEs => 'Español';
String get appSettings_languageEs => 'Español';
@override
String get appSettings_languageDe => 'Deutsch';
@ -467,16 +460,16 @@ class AppLocalizationsEn extends AppLocalizations {
String get appSettings_languagePl => 'Polski';
@override
String get appSettings_languageSl => 'Slovenščina';
String get appSettings_languageSl => 'Slovenščina';
@override
String get appSettings_languagePt => 'Português';
String get appSettings_languagePt => 'Português';
@override
String get appSettings_languageIt => 'Italiano';
@override
String get appSettings_languageZh => '中文';
String get appSettings_languageZh => '中文';
@override
String get appSettings_languageSv => 'Svenska';
@ -485,16 +478,16 @@ class AppLocalizationsEn extends AppLocalizations {
String get appSettings_languageNl => 'Nederlands';
@override
String get appSettings_languageSk => 'Slovenčina';
String get appSettings_languageSk => 'Slovenčina';
@override
String get appSettings_languageBg => 'Български';
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Русский';
String get appSettings_languageRu => 'Русский';
@override
String get appSettings_languageUk => 'Українська';
String get appSettings_languageUk => 'Українська';
@override
String get appSettings_enableMessageTracing => 'Enable Message Tracing';
@ -2431,7 +2424,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String telemetry_temperatureValue(String celsius, String fahrenheit) {
return '$celsius°C / $fahrenheit°F';
return '$celsius°C / $fahrenheit°F';
}
@override
@ -2499,7 +2492,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String channelPath_observedPathTitle(int index, String hops) {
return 'Observed path $index $hops';
return 'Observed path $index • $hops';
}
@override
@ -2554,7 +2547,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String channelPath_selectedPathLabel(String label, String prefixes) {
return '$label $prefixes';
return '$label • $prefixes';
}
@override

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -93,7 +93,7 @@ class AppLocalizationsIt extends AppLocalizations {
String get common_loading => 'Caricamento...';
@override
String get common_notAvailable => '';
String get common_notAvailable => '—';
@override
String common_voltageValue(String volts) {
@ -108,14 +108,6 @@ class AppLocalizationsIt extends AppLocalizations {
@override
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
String get connectionChoiceUsbLabel => 'USB';
@ -134,7 +126,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
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
String get usbScreenEmptyState =>
@ -176,7 +168,7 @@ class AppLocalizationsIt extends AppLocalizations {
String get scanner_scan => 'Scansiona';
@override
String get scanner_bluetoothOff => 'Il Bluetooth è disattivato.';
String get scanner_bluetoothOff => 'Il Bluetooth è disattivato.';
@override
String get scanner_bluetoothOffMessage =>
@ -273,7 +265,7 @@ class AppLocalizationsIt extends AppLocalizations {
String get settings_longitude => 'Longitudine';
@override
String get settings_privacyMode => 'Modalità Privacy';
String get settings_privacyMode => 'Modalità Privacy';
@override
String get settings_privacyModeSubtitle =>
@ -281,13 +273,13 @@ class AppLocalizationsIt extends AppLocalizations {
@override
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
String get settings_privacyModeEnabled => 'Modalità privacy abilitata';
String get settings_privacyModeEnabled => 'Modalità privacy abilitata';
@override
String get settings_privacyModeDisabled => 'Modalità privacy disabilitata';
String get settings_privacyModeDisabled => 'Modalità privacy disabilitata';
@override
String get settings_actions => 'Azioni';
@ -425,7 +417,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
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
String settings_error(String message) {
@ -460,10 +452,10 @@ class AppLocalizationsIt extends AppLocalizations {
String get appSettings_languageEn => 'English';
@override
String get appSettings_languageFr => 'Français';
String get appSettings_languageFr => 'Français';
@override
String get appSettings_languageEs => 'Español';
String get appSettings_languageEs => 'Español';
@override
String get appSettings_languageDe => 'Deutsch';
@ -472,16 +464,16 @@ class AppLocalizationsIt extends AppLocalizations {
String get appSettings_languagePl => 'Polski';
@override
String get appSettings_languageSl => 'Slovenščina';
String get appSettings_languageSl => 'Slovenščina';
@override
String get appSettings_languagePt => 'Português';
String get appSettings_languagePt => 'Português';
@override
String get appSettings_languageIt => 'Italiano';
@override
String get appSettings_languageZh => '中文';
String get appSettings_languageZh => '中文';
@override
String get appSettings_languageSv => 'Svenska';
@ -490,10 +482,10 @@ class AppLocalizationsIt extends AppLocalizations {
String get appSettings_languageNl => 'Nederlands';
@override
String get appSettings_languageSk => 'Slovenčina';
String get appSettings_languageSk => 'Slovenčina';
@override
String get appSettings_languageBg => 'Български';
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Russo';
@ -576,7 +568,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get appSettings_autoRouteRotationSubtitle =>
'Alterna tra i percorsi migliori e la modalità alluvione';
'Alterna tra i percorsi migliori e la modalità alluvione';
@override
String get appSettings_autoRouteRotationEnabled =>
@ -671,7 +663,7 @@ class AppLocalizationsIt extends AppLocalizations {
String get appSettings_offlineMapCache => 'Cache Mappa Offline';
@override
String get appSettings_unitsTitle => 'Unità';
String get appSettings_unitsTitle => 'Unità';
@override
String get appSettings_unitsMetric => 'Metrico (m/km)';
@ -790,11 +782,12 @@ class AppLocalizationsIt extends AppLocalizations {
String get contacts_groupName => 'Nome gruppo';
@override
String get contacts_groupNameRequired => 'Il nome del gruppo è obbligatorio.';
String get contacts_groupNameRequired =>
'Il nome del gruppo è obbligatorio.';
@override
String contacts_groupAlreadyExists(String name) {
return 'Il gruppo \"$name\" esiste già.';
return 'Il gruppo \"$name\" esiste già.';
}
@override
@ -880,7 +873,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String channels_deleteChannelConfirm(String name) {
return 'Eliminare \"$name\"? Non può essere annullato.';
return 'Eliminare \"$name\"? Non può essere annullato.';
}
@override
@ -977,20 +970,20 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get channels_joinPublicChannelDesc =>
'Chiunque può unirsi a questo canale.';
'Chiunque può unirsi a questo canale.';
@override
String get channels_joinHashtagChannel => 'Unisciti a un Canale con Hashtag';
@override
String get channels_joinHashtagChannelDesc =>
'Chiunque può unirsi ai canali hashtag.';
'Chiunque può unirsi ai canali hashtag.';
@override
String get channels_scanQrCode => 'Scansiona un codice QR';
@override
String get channels_scanQrCodeComingSoon => 'Arriverà presto';
String get channels_scanQrCodeComingSoon => 'Arriverà presto';
@override
String get channels_enterHashtag => 'Inserisci hashtag';
@ -1124,7 +1117,7 @@ class AppLocalizationsIt extends AppLocalizations {
String get debugLog_rawLogRx => 'Log Raw-RX';
@override
String get debugLog_noBleActivity => 'Nessuna attività BLE rilevata ancora.';
String get debugLog_noBleActivity => 'Nessuna attività BLE rilevata ancora.';
@override
String debugFrame_length(int count) {
@ -1180,20 +1173,20 @@ class AppLocalizationsIt extends AppLocalizations {
String get chat_ShowAllPaths => 'Mostra tutti i percorsi';
@override
String get chat_routingMode => 'Modalità di routing';
String get chat_routingMode => 'Modalità di routing';
@override
String get chat_autoUseSavedPath => 'Utilizza il percorso salvato';
@override
String get chat_forceFloodMode => 'Modalità Inondamento Forzato';
String get chat_forceFloodMode => 'Modalità Inondamento Forzato';
@override
String get chat_recentAckPaths => 'Percorsi ACK Recenti (tocca per usare):';
@override
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
String get chat_hopSingular => 'salta';
@ -1220,7 +1213,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
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
String get chat_pathActions => 'Azioni Percorso:';
@ -1241,7 +1234,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get chat_pathCleared =>
'Percorso sgomberato. Il prossimo messaggio riidentifierà il percorso.';
'Percorso sgomberato. Il prossimo messaggio riidentifierà il percorso.';
@override
String get chat_floodModeSubtitle =>
@ -1249,7 +1242,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
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
String get chat_fullPath => 'Percorso Completo';
@ -1424,7 +1417,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
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
@ -1642,7 +1635,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
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
String get login_repeaterDescription =>
@ -1656,13 +1649,13 @@ class AppLocalizationsIt extends AppLocalizations {
String get login_routing => 'Instradamento';
@override
String get login_routingMode => 'Modalità di routing';
String get login_routingMode => 'Modalità di routing';
@override
String get login_autoUseSavedPath => 'Utilizza il percorso salvato';
@override
String get login_forceFloodMode => 'Modalità Inondamento Forzato';
String get login_forceFloodMode => 'Modalità Inondamento Forzato';
@override
String get login_managePaths => 'Gestisci Percorsi';
@ -1682,7 +1675,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
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
String get common_reload => 'Ricaricare';
@ -1725,7 +1718,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
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
String get path_selectFromContacts => 'Seleziona da contatti:';
@ -1745,7 +1738,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get path_tooLong =>
'Il percorso è troppo lungo. Massimo 64 salti consentiti.';
'Il percorso è troppo lungo. Massimo 64 salti consentiti.';
@override
String get path_setPath => 'Imposta Percorso';
@ -1797,13 +1790,13 @@ class AppLocalizationsIt extends AppLocalizations {
String get repeater_statusTitle => 'Stato del Ripetitore';
@override
String get repeater_routingMode => 'Modalità di routing';
String get repeater_routingMode => 'Modalità di routing';
@override
String get repeater_autoUseSavedPath => 'Percorso salvato automatico';
@override
String get repeater_forceFloodMode => 'Modalità Inondamento Forzato';
String get repeater_forceFloodMode => 'Modalità Inondamento Forzato';
@override
String get repeater_pathManagement => 'Gestione dei percorsi';
@ -1829,7 +1822,7 @@ class AppLocalizationsIt extends AppLocalizations {
String get repeater_clockAtLogin => 'Orologio (all\'accesso)';
@override
String get repeater_uptime => 'Disponibilità';
String get repeater_uptime => 'Disponibilità';
@override
String get repeater_queueLength => 'Lunghezza della coda';
@ -1981,7 +1974,7 @@ class AppLocalizationsIt extends AppLocalizations {
'Consenti l\'accesso ospite in sola lettura';
@override
String get repeater_privacyMode => 'Modalità Privacy';
String get repeater_privacyMode => 'Modalità Privacy';
@override
String get repeater_privacyModeSubtitle =>
@ -1991,7 +1984,7 @@ class AppLocalizationsIt extends AppLocalizations {
String get repeater_advertisementSettings => 'Impostazioni Annuncio';
@override
String get repeater_localAdvertInterval => 'Intervallo Pubblicità Locale';
String get repeater_localAdvertInterval => 'Intervallo Pubblicità Locale';
@override
String repeater_localAdvertIntervalMinutes(int minutes) {
@ -2000,7 +1993,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get repeater_floodAdvertInterval =>
'Intervallo Pubblicità Inondazione';
'Intervallo Pubblicità Inondazione';
@override
String repeater_floodAdvertIntervalHours(int hours) {
@ -2026,7 +2019,7 @@ class AppLocalizationsIt extends AppLocalizations {
'Sei sicuro di voler riavviare questo ripetitore?';
@override
String get repeater_regenerateIdentityKey => 'Rigenera Chiave Identità';
String get repeater_regenerateIdentityKey => 'Rigenera Chiave Identità';
@override
String get repeater_regenerateIdentityKeySubtitle =>
@ -2034,7 +2027,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get repeater_regenerateIdentityKeyConfirm =>
'Questo genererà una nuova identità per il ripetitore. Procedere?';
'Questo genererà una nuova identità per il ripetitore. Procedere?';
@override
String get repeater_eraseFileSystem => 'Elimina File System';
@ -2045,11 +2038,11 @@ class AppLocalizationsIt extends AppLocalizations {
@override
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
String get repeater_eraseSerialOnly =>
'Elimina è disponibile solo tramite console seriale.';
'Elimina è disponibile solo tramite console seriale.';
@override
String repeater_commandSent(String command) {
@ -2093,7 +2086,7 @@ class AppLocalizationsIt extends AppLocalizations {
String get repeater_refreshGuestAccess => 'Aggiorna Accesso Ospite';
@override
String get repeater_refreshPrivacyMode => 'Aggiorna Modalità Privacy';
String get repeater_refreshPrivacyMode => 'Aggiorna Modalità Privacy';
@override
String get repeater_refreshAdvertisementSettings =>
@ -2174,7 +2167,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get repeater_cliHelpReboot =>
'Riavvia il dispositivo. (nota, potresti ottenere \'Timeout\' che è normale)';
'Riavvia il dispositivo. (nota, potresti ottenere \'Timeout\' che è normale)';
@override
String get repeater_cliHelpClock =>
@ -2206,7 +2199,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
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
String get repeater_cliHelpSetFloodMax =>
@ -2214,7 +2207,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
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
String get repeater_cliHelpSetAgcResetInterval =>
@ -2226,7 +2219,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
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
String get repeater_cliHelpSetFloodAdvertInterval =>
@ -2257,11 +2250,11 @@ class AppLocalizationsIt extends AppLocalizations {
@override
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
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
String get repeater_cliHelpSetBridgeEnabled => 'Abilita/Disabilita ponte.';
@ -2272,11 +2265,11 @@ class AppLocalizationsIt extends AppLocalizations {
@override
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
String get repeater_cliHelpSetBridgeBaud =>
'Imposta la velocità di trasmissione per i ponti rs232.';
'Imposta la velocità di trasmissione per i ponti rs232.';
@override
String get repeater_cliHelpSetBridgeSecret =>
@ -2292,7 +2285,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
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
String get repeater_cliHelpGetBridgeType =>
@ -2312,7 +2305,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
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
String get repeater_cliHelpNeighborRemove =>
@ -2324,7 +2317,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
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
String get repeater_cliHelpRegionGet =>
@ -2344,7 +2337,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
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
String get repeater_cliHelpRegionHome =>
@ -2359,7 +2352,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
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
String get repeater_cliHelpGpsOnOff =>
@ -2416,7 +2409,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
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
String get telemetry_receivedData => 'Dati Telemetria Ricevuti';
@ -2469,7 +2462,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String telemetry_temperatureValue(String celsius, String fahrenheit) {
return '$celsius°C / $fahrenheit°F';
return '$celsius°C / $fahrenheit°F';
}
@override
@ -2537,7 +2530,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String channelPath_observedPathTitle(int index, String hops) {
return 'Percorso osservato $index $hops';
return 'Percorso osservato $index • $hops';
}
@override
@ -2592,7 +2585,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String channelPath_selectedPathLabel(String label, String prefixes) {
return '$label $prefixes';
return '$label • $prefixes';
}
@override
@ -2603,14 +2596,14 @@ class AppLocalizationsIt extends AppLocalizations {
String get channelPath_unknownRepeater => 'Ripetitore sconosciuto';
@override
String get community_title => 'Comunità';
String get community_title => 'Comunità';
@override
String get community_create => 'Crea Comunità';
String get community_create => 'Crea Comunità';
@override
String get community_createDesc =>
'Crea una nuova comunità e condividila tramite codice QR.';
'Crea una nuova comunità e condividila tramite codice QR.';
@override
String get community_join => 'Unisciti';
@ -2628,35 +2621,35 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get community_scanInstructions =>
'Punta la fotocamera su un codice QR della comunità';
'Punta la fotocamera su un codice QR della comunità';
@override
String get community_showQr => 'Mostra il codice QR';
@override
String get community_publicChannel => 'Comunità Pubblica';
String get community_publicChannel => 'Comunità Pubblica';
@override
String get community_hashtagChannel => 'Hashtag della Comunità';
String get community_hashtagChannel => 'Hashtag della Comunità';
@override
String get community_name => 'Nome della Comunità';
String get community_name => 'Nome della Comunità';
@override
String get community_enterName => 'Inserisci il nome della comunità';
String get community_enterName => 'Inserisci il nome della comunità';
@override
String community_created(String name) {
return 'Comunità \"$name\" creata';
return 'Comunità \"$name\" creata';
}
@override
String community_joined(String name) {
return 'Unito alla comunità \"$name\"';
return 'Unito alla comunità \"$name\"';
}
@override
String get community_qrTitle => 'Condividi Comunità';
String get community_qrTitle => 'Condividi Comunità';
@override
String community_qrInstructions(String name) {
@ -2671,16 +2664,16 @@ class AppLocalizationsIt extends AppLocalizations {
String get community_invalidQrCode => 'Codice QR della community non valido';
@override
String get community_alreadyMember => 'Già membro';
String get community_alreadyMember => 'Già membro';
@override
String community_alreadyMemberMessage(String name) {
return 'Sei già un membro di \"$name\".';
return 'Sei già un membro di \"$name\".';
}
@override
String get community_addPublicChannel =>
'Aggiungi Canale Pubblico della Comunità';
'Aggiungi Canale Pubblico della Comunità';
@override
String get community_addPublicChannelHint =>
@ -2694,10 +2687,10 @@ class AppLocalizationsIt extends AppLocalizations {
'Scansiona un codice QR o crea una community per iniziare.';
@override
String get community_manageCommunities => 'Gestisci Comunità';
String get community_manageCommunities => 'Gestisci Comunità';
@override
String get community_delete => 'Lascia la Comunità';
String get community_delete => 'Lascia la Comunità';
@override
String community_deleteConfirm(String name) {
@ -2706,12 +2699,12 @@ class AppLocalizationsIt extends AppLocalizations {
@override
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
String community_deleted(String name) {
return 'Hai lasciato la comunità \"$name\"';
return 'Hai lasciato la comunità \"$name\"';
}
@override
@ -2751,21 +2744,21 @@ class AppLocalizationsIt extends AppLocalizations {
'Aggiungi un canale con hashtag per questa community';
@override
String get community_selectCommunity => 'Seleziona Comunità';
String get community_selectCommunity => 'Seleziona Comunità';
@override
String get community_regularHashtag => 'Hashtag regolare';
@override
String get community_regularHashtagDesc =>
'Hashtag pubblico (chiunque può unirsi)';
'Hashtag pubblico (chiunque può unirsi)';
@override
String get community_communityHashtag => 'Hashtag della Comunità';
String get community_communityHashtag => 'Hashtag della Comunità';
@override
String get community_communityHashtagDesc =>
'Visibile solo ai membri della comunità';
'Visibile solo ai membri della comunità';
@override
String community_forCommunity(String name) {
@ -2832,7 +2825,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get pathTrace_someHopsNoLocation =>
'Uno o più dei luppoli mancano di una posizione!';
'Uno o più dei luppoli mancano di una posizione!';
@override
String get pathTrace_clearTooltip => 'Pulisci percorso';
@ -2854,7 +2847,7 @@ class AppLocalizationsIt extends AppLocalizations {
'Eseguire LOS per visualizzare il profilo altimetrico';
@override
String get losMenuTitle => 'Menù LOS';
String get losMenuTitle => 'Menù LOS';
@override
String get losMenuSubtitle =>
@ -2926,7 +2919,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get losErrorElevationUnavailable =>
'Dati di elevazione non disponibili per uno o più campioni.';
'Dati di elevazione non disponibili per uno o più campioni.';
@override
String get losErrorInvalidInput =>
@ -2964,7 +2957,7 @@ class AppLocalizationsIt extends AppLocalizations {
String get losFrequencyInfoTooltip => 'Visualizza i dettagli del calcolo';
@override
String get losFrequencyDialogTitle => 'Calcolo dellorizzonte radio';
String get losFrequencyDialogTitle => 'Calcolo dell’orizzonte radio';
@override
String losFrequencyDialogDescription(
@ -3004,13 +2997,13 @@ class AppLocalizationsIt extends AppLocalizations {
}
@override
String get contacts_clipboardEmpty => 'La clipboard è vuota.';
String get contacts_clipboardEmpty => 'La clipboard è vuota.';
@override
String get contacts_invalidAdvertFormat => 'Dati di contatto non validi';
@override
String get contacts_contactImported => 'Il contatto è stato importato.';
String get contacts_contactImported => 'Il contatto è stato importato.';
@override
String get contacts_contactImportFailed =>
@ -3052,7 +3045,7 @@ class AppLocalizationsIt extends AppLocalizations {
'Copia dell\'annuncio nella Clipboard non riuscita.';
@override
String get notification_activityTitle => 'Attività MeshCore';
String get notification_activityTitle => 'Attività MeshCore';
@override
String notification_messagesCount(int count) {
@ -3130,7 +3123,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get settings_gpxExportError =>
'Si è verificato un errore durante l\'esportazione.';
'Si è verificato un errore durante l\'esportazione.';
@override
String get settings_gpxExportRepeatersRoom =>

View file

@ -69,7 +69,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get common_share => 'Delen';
@override
String get common_copy => 'Kopiëren';
String get common_copy => 'Kopiëren';
@override
String get common_retry => 'Nogmaals proberen';
@ -93,7 +93,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get common_loading => 'Laden...';
@override
String get common_notAvailable => '';
String get common_notAvailable => '—';
@override
String common_voltageValue(String volts) {
@ -108,13 +108,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override
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
String get connectionChoiceUsbLabel => 'USB';
@ -126,7 +119,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
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
String get usbScreenStatus => 'Selecteer een USB-apparaat';
@ -238,7 +231,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_location => 'Locatie';
@override
String get settings_locationSubtitle => 'GPS coördinaten';
String get settings_locationSubtitle => 'GPS coördinaten';
@override
String get settings_locationUpdated => 'Locatie bijgewerkt';
@ -457,10 +450,10 @@ class AppLocalizationsNl extends AppLocalizations {
String get appSettings_languageEn => 'English';
@override
String get appSettings_languageFr => 'Français';
String get appSettings_languageFr => 'Français';
@override
String get appSettings_languageEs => 'Español';
String get appSettings_languageEs => 'Español';
@override
String get appSettings_languageDe => 'Deutsch';
@ -469,16 +462,16 @@ class AppLocalizationsNl extends AppLocalizations {
String get appSettings_languagePl => 'Polski';
@override
String get appSettings_languageSl => 'Slovenščina';
String get appSettings_languageSl => 'Slovenščina';
@override
String get appSettings_languagePt => 'Português';
String get appSettings_languagePt => 'Português';
@override
String get appSettings_languageIt => 'Italiano';
@override
String get appSettings_languageZh => '中文';
String get appSettings_languageZh => '中文';
@override
String get appSettings_languageSv => 'Svenska';
@ -487,16 +480,16 @@ class AppLocalizationsNl extends AppLocalizations {
String get appSettings_languageNl => 'Nederlands';
@override
String get appSettings_languageSk => 'Slovenčina';
String get appSettings_languageSk => 'Slovenčina';
@override
String get appSettings_languageBg => 'Български';
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Russisch';
@override
String get appSettings_languageUk => 'Oekraïens';
String get appSettings_languageUk => 'Oekraïens';
@override
String get appSettings_enableMessageTracing => 'Berichttracking inschakelen';
@ -854,7 +847,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get channels_public => 'Openbaar';
@override
String get channels_private => 'Privé';
String get channels_private => 'Privé';
@override
String get channels_publicChannel => 'Open kanaal';
@ -954,14 +947,14 @@ class AppLocalizationsNl extends AppLocalizations {
String get channels_sortUnread => 'Ongelezen';
@override
String get channels_createPrivateChannel => 'Maak een Privé Kanaal';
String get channels_createPrivateChannel => 'Maak een Privé Kanaal';
@override
String get channels_createPrivateChannelDesc =>
'Beveiligd met een geheime sleutel.';
@override
String get channels_joinPrivateChannel => 'Sluit een Privé Kanaal aan';
String get channels_joinPrivateChannel => 'Sluit een Privé Kanaal aan';
@override
String get channels_joinPrivateChannelDesc =>
@ -1343,7 +1336,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
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
String map_nodesCount(int count) {
@ -1371,7 +1364,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get map_pinDm => 'Verzenden als bericht (DM)';
@override
String get map_pinPrivate => 'Beveiligd (Privé)';
String get map_pinPrivate => 'Beveiligd (Privé)';
@override
String get map_pinPublic => 'Openbaar spikken';
@ -2038,7 +2031,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get repeater_eraseSerialOnly =>
'Verwijderen is alleen beschikbaar via de seriële console.';
'Verwijderen is alleen beschikbaar via de seriële console.';
@override
String repeater_commandSent(String command) {
@ -2266,7 +2259,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
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
String get repeater_cliHelpSetBridgeSecret =>
@ -2282,7 +2275,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
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
String get repeater_cliHelpGetBridgeType =>
@ -2314,7 +2307,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
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
String get repeater_cliHelpRegionGet =>
@ -2359,7 +2352,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
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
String get repeater_cliHelpGpsAdvert =>
@ -2397,14 +2390,14 @@ class AppLocalizationsNl extends AppLocalizations {
@override
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
String get repeater_gpsManagement => 'Beheer GPS';
@override
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
String get telemetry_receivedData => 'Ontvangen Telemetriedata';
@ -2457,7 +2450,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String telemetry_temperatureValue(String celsius, String fahrenheit) {
return '$celsius°C / $fahrenheit°F';
return '$celsius°C / $fahrenheit°F';
}
@override
@ -2526,7 +2519,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String channelPath_observedPathTitle(int index, String hops) {
return 'Waargenomen pad $index $hops';
return 'Waargenomen pad $index • $hops';
}
@override
@ -2581,7 +2574,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String channelPath_selectedPathLabel(String label, String prefixes) {
return '$label $prefixes';
return '$label • $prefixes';
}
@override
@ -2998,11 +2991,11 @@ class AppLocalizationsNl extends AppLocalizations {
String get contacts_invalidAdvertFormat => 'Ongeldige contactgegevens';
@override
String get contacts_contactImported => 'Contact is geïmporteerd.';
String get contacts_contactImported => 'Contact is geïmporteerd.';
@override
String get contacts_contactImportFailed =>
'Contact kon niet geïmporteerd worden.';
'Contact kon niet geïmporteerd worden.';
@override
String get contacts_zeroHopAdvert => 'Zero Hop Reclame';
@ -3011,14 +3004,14 @@ class AppLocalizationsNl extends AppLocalizations {
String get contacts_floodAdvert => 'Overstromingsadvertentie';
@override
String get contacts_copyAdvertToClipboard => 'Advert naar klembord kopiëren';
String get contacts_copyAdvertToClipboard => 'Advert naar klembord kopiëren';
@override
String get contacts_addContactFromClipboard =>
'Contact uit klembord toevoegen';
@override
String get contacts_ShareContact => 'Kontakt naar Klembord kopiëren';
String get contacts_ShareContact => 'Kontakt naar Klembord kopiëren';
@override
String get contacts_ShareContactZeroHop => 'Contact delen via advertentie';
@ -3037,7 +3030,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get contacts_contactAdvertCopyFailed =>
'Kopiëren van advertentie naar Clipboard is mislukt.';
'Kopiëren van advertentie naar Clipboard is mislukt.';
@override
String get notification_activityTitle => 'MeshCore Activiteit';
@ -3106,7 +3099,8 @@ class AppLocalizationsNl extends AppLocalizations {
'Exporteert alle contacten met een locatie naar een GPX-bestand.';
@override
String get settings_gpxExportSuccess => 'Succesvol GPX-bestand geëxporteerd.';
String get settings_gpxExportSuccess =>
'Succesvol GPX-bestand geëxporteerd.';
@override
String get settings_gpxExportNoContacts => 'Geen contacten om te exporteren.';
@ -3130,7 +3124,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get settings_gpxExportShareText =>
'Kaartgegevens geëxporteerd uit meshcore-open';
'Kaartgegevens geëxporteerd uit meshcore-open';
@override
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

View file

@ -1,4 +1,4 @@
{
{
"channels_channelDeleteFailed": "Kan kanaal {name} niet verwijderen",
"@channels_channelDeleteFailed": {
"placeholders": {
@ -27,7 +27,7 @@
"common_create": "Maak",
"common_continue": "Doorgaan",
"common_share": "Delen",
"common_copy": "Kopiëren",
"common_copy": "Kopiëren",
"common_retry": "Nogmaals proberen",
"common_hide": "Verbergen",
"common_remove": "Verwijderen",
@ -35,7 +35,7 @@
"common_disable": "Uitschakelen",
"common_reboot": "Herstarten",
"common_loading": "Laden...",
"common_notAvailable": "",
"common_notAvailable": "—",
"common_voltageValue": "{volts} V",
"@common_voltageValue": {
"placeholders": {
@ -92,7 +92,7 @@
"settings_radioSettingsSubtitle": "Frequentie, vermogen, spredfactor",
"settings_radioSettingsUpdated": "Radio instellingen bijgewerkt",
"settings_location": "Locatie",
"settings_locationSubtitle": "GPS coördinaten",
"settings_locationSubtitle": "GPS coördinaten",
"settings_locationUpdated": "Locatie bijgewerkt",
"settings_locationBothRequired": "Voer zowel breedte- als lengtegraad in.",
"settings_locationInvalid": "Ongeldige breedtegraad of lengtegraad.",
@ -165,18 +165,18 @@
"appSettings_language": "Taal",
"appSettings_languageSystem": "Standaardinstelling",
"appSettings_languageEn": "English",
"appSettings_languageFr": "Français",
"appSettings_languageEs": "Español",
"appSettings_languageFr": "Français",
"appSettings_languageEs": "Español",
"appSettings_languageDe": "Deutsch",
"appSettings_languagePl": "Polski",
"appSettings_languageSl": "Slovenščina",
"appSettings_languagePt": "Português",
"appSettings_languageSl": "Slovenščina",
"appSettings_languagePt": "Português",
"appSettings_languageIt": "Italiano",
"appSettings_languageZh": "中文",
"appSettings_languageZh": "中文",
"appSettings_languageSv": "Svenska",
"appSettings_languageNl": "Nederlands",
"appSettings_languageSk": "Slovenčina",
"appSettings_languageBg": "Български",
"appSettings_languageSk": "Slovenčina",
"appSettings_languageBg": "Български",
"appSettings_notifications": "Notificaties",
"appSettings_enableNotifications": "Notificaties inschakelen",
"appSettings_enableNotificationsSubtitle": "Ontvang meldingen voor berichten en advertenties",
@ -338,7 +338,7 @@
},
"channels_hashtagChannel": "Hashtag kanaal",
"channels_public": "Openbaar",
"channels_private": "Privé",
"channels_private": "Privé",
"channels_publicChannel": "Open kanaal",
"channels_privateChannel": "Private kanaal",
"channels_editChannel": "Kanaal bewerken",
@ -623,7 +623,7 @@
"chat_invalidLink": "Ongeldig linkformaat",
"map_title": "Node Map",
"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": {
"placeholders": {
@ -645,7 +645,7 @@
"map_room": "Ruimte",
"map_sensor": "Sensor",
"map_pinDm": "Verzenden als bericht (DM)",
"map_pinPrivate": "Beveiligd (Privé)",
"map_pinPrivate": "Beveiligd (Privé)",
"map_pinPublic": "Openbaar spikken",
"map_lastSeen": "Laaste keer gezien",
"map_disconnectConfirm": "Ben je er zeker van dat je verbinding met dit apparaat wilt verbreken?",
@ -1039,7 +1039,7 @@
"repeater_eraseFileSystem": "Verwijder Besturingssysteem",
"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_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": {
"placeholders": {
@ -1143,11 +1143,11 @@
"repeater_cliHelpSetBridgeEnabled": "Poort inschakelen/uitschakelen.",
"repeater_cliHelpSetBridgeDelay": "Verzend vertraging instellen voor pakketten.",
"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_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_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_cliHelpLogStart": "Start pakketlogging 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_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_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_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)",
@ -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_cliHelpGpsOnOff": "Schakel de GPS-standby aan/uit.",
"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_cliHelpGpsAdvertSet": "Stelt advertentie locatie configuratie in.",
"repeater_commandsListTitle": "Commandenlijst",
@ -1178,9 +1178,9 @@
"repeater_logging": "Logging",
"repeater_neighborsRepeaterOnly": "Buren (Alleen repeaters)",
"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_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_requestTimeout": "Telemetryverzoek is uitgevallen.",
"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": {
"placeholders": {
"celsius": {
@ -1254,7 +1254,7 @@
"channelPath_repeatsLabel": "Repeats",
"channelPath_pathLabel": "Pad {index}",
"channelPath_observedLabel": "Waargenomen",
"channelPath_observedPathTitle": "Waargenomen pad {index} {hops}",
"channelPath_observedPathTitle": "Waargenomen pad {index} • {hops}",
"@channelPath_observedPathTitle": {
"placeholders": {
"index": {
@ -1329,7 +1329,7 @@
},
"channelPath_pathLabelTitle": "Pad",
"channelPath_observedPathHeader": "Waargenomen Pad",
"channelPath_selectedPathLabel": "{label} {prefixes}",
"channelPath_selectedPathLabel": "{label} • {prefixes}",
"@channelPath_selectedPathLabel": {
"placeholders": {
"label": {
@ -1369,8 +1369,8 @@
"neighbors_repeatersNeighbors": "Herhalingen Buren",
"neighbors_noData": "Geen gegevens van buren beschikbaar.",
"channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.",
"channels_createPrivateChannel": "Maak een Privé Kanaal",
"channels_joinPrivateChannel": "Sluit een Privé Kanaal aan",
"channels_createPrivateChannel": "Maak een Privé Kanaal",
"channels_joinPrivateChannel": "Sluit een Privé Kanaal aan",
"channels_joinPrivateChannelDesc": "Handmatig een geheime sleutel invoeren.",
"channels_joinPublicChannel": "Sluit het Open Kanaal",
"channels_joinPublicChannelDesc": "Iedereen kan dit kanaal aanmelden.",
@ -1558,22 +1558,22 @@
"contacts_roomPing": "Ping kamer server",
"contacts_chatTraceRoute": "Route traceren",
"contacts_pathTraceTo": "Trace route to {name}",
"appSettings_languageUk": "Oekraïens",
"appSettings_languageUk": "Oekraïens",
"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_floodAdvert": "Overstromingsadvertentie",
"contacts_copyAdvertToClipboard": "Advert naar klembord kopiëren",
"contacts_copyAdvertToClipboard": "Advert naar klembord kopiëren",
"appSettings_languageRu": "Russisch",
"appSettings_enableMessageTracing": "Berichttracking inschakelen",
"appSettings_enableMessageTracingSubtitle": "Gedetailleerde routerings- en timing-metadata voor berichten weergeven",
"contacts_clipboardEmpty": "Knipbord is leeg.",
"contacts_addContactFromClipboard": "Contact uit klembord toevoegen",
"contacts_contactImported": "Contact is geïmporteerd.",
"contacts_contactImported": "Contact is geïmporteerd.",
"contacts_zeroHopContactAdvertSent": "Contact verzonden via advertentie",
"contacts_contactAdvertCopied": "Reclame gekopieerd naar Klembord.",
"contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.",
"contacts_ShareContact": "Kontakt naar Klembord kopiëren",
"contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.",
"contacts_ShareContact": "Kontakt naar Klembord kopiëren",
"contacts_ShareContactZeroHop": "Contact delen via advertentie",
"contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden",
"notification_activityTitle": "MeshCore Activiteit",
@ -1584,7 +1584,7 @@
"notification_receivedNewMessage": "Nieuw bericht ontvangen",
"settings_gpxExportRepeatersSubtitle": "Exporteert repeaters / roomserver met een locatie naar GPX-bestand.",
"settings_gpxExportRepeaters": "Exporteer repeaters / roomserver naar GPX",
"settings_gpxExportSuccess": "Succesvol GPX-bestand geëxporteerd.",
"settings_gpxExportSuccess": "Succesvol GPX-bestand geëxporteerd.",
"settings_gpxExportNoContacts": "Geen contacten om te exporteren.",
"settings_gpxExportNotAvailable": "Niet ondersteund op uw apparaat/besturingssysteem",
"settings_gpxExportError": "Er was een fout bij het exporteren.",
@ -1595,7 +1595,7 @@
"settings_gpxExportRepeatersRoom": "Repeater- en kamer servers locaties",
"settings_gpxExportChat": "Locaties van metgezellen",
"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",
"pathTrace_someHopsNoLocation": "Een of meer van de hops ontbreken een locatie!",
"map_removeLast": "Verwijder Laatste",
@ -1802,11 +1802,9 @@
"contacts_searchUsers": "Zoek {number}{str} gebruikers...",
"contacts_searchFavorites": "Zoek {number}{str} favorieten...",
"contacts_searchRoomServers": "Zoek {number}{str} Room servers...",
"connectionChoiceTitle": "Kies uw verbindingsmethode",
"connectionChoiceUsbLabel": "USB",
"connectionChoiceSubtitle": "Kies hoe u uw MeshCore-apparaat wilt bereiken.",
"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",
"usbScreenNote": "USB-serieel is actief op ondersteunde Android-apparaten en desktop-platforms.",
"usbScreenTitle": "Verbind via USB",

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

View file

@ -8,7 +8,7 @@ import 'screens/chrome_required_screen.dart';
import 'utils/platform_info.dart';
import 'connector/meshcore_connector.dart';
import 'screens/connection_choice_screen.dart';
import 'screens/scanner_screen.dart';
import 'services/storage_service.dart';
import 'services/message_retry_service.dart';
import 'services/path_history_service.dart';
@ -192,7 +192,7 @@ class MeshCoreApp extends StatelessWidget {
},
home: (PlatformInfo.isWeb && !PlatformInfo.isChrome)
? const ChromeRequiredScreen()
: const ConnectionChoiceScreen(),
: const ScannerScreen(),
);
},
),

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,
),
),
);
},
),
);
}
}

View file

@ -9,6 +9,7 @@ import '../l10n/l10n.dart';
import '../widgets/adaptive_app_bar_title.dart';
import '../widgets/device_tile.dart';
import 'contacts_screen.dart';
import 'usb_screen.dart';
/// Screen for scanning and connecting to MeshCore devices
class ScannerScreen extends StatefulWidget {
@ -114,40 +115,67 @@ class _ScannerScreenState extends State<ScannerScreen> {
},
),
),
floatingActionButton: Consumer<MeshCoreConnector>(
bottomNavigationBar: Consumer<MeshCoreConnector>(
builder: (context, connector, child) {
final isScanning =
connector.state == MeshCoreConnectionState.scanning;
final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off;
final usbSupported = PlatformInfo.supportsUsbSerial;
return FloatingActionButton.extended(
onPressed: isBluetoothOff
? null
: () {
if (isScanning) {
connector.stopScan();
} else {
unawaited(
connector.startScan().catchError((e) {
debugPrint("Scanner screen startScan error: $e");
}),
return SafeArea(
top: false,
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (usbSupported)
FloatingActionButton.extended(
onPressed: () {
debugPrint(
'ScannerScreen: USB selected, opening UsbScreen',
);
}
},
icon: isScanning
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.bluetooth_searching),
label: Text(
isScanning
? context.l10n.scanner_stop
: context.l10n.scanner_scan,
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const UsbScreen()),
);
},
heroTag: 'scanner_usb_action',
icon: const Icon(Icons.usb),
label: Text(context.l10n.connectionChoiceUsbLabel),
),
if (usbSupported) const SizedBox(width: 12),
FloatingActionButton.extended(
heroTag: 'scanner_ble_action',
onPressed: isBluetoothOff
? null
: () {
if (isScanning) {
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,
),
),
],
),
);
},

View file

@ -5,10 +5,12 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_connector_usb.dart';
import '../l10n/l10n.dart';
import '../utils/platform_info.dart';
import '../utils/usb_port_labels.dart';
import 'contacts_screen.dart';
import 'scanner_screen.dart';
class UsbScreen extends StatefulWidget {
const UsbScreen({super.key});
@ -28,6 +30,7 @@ class _UsbScreenState extends State<UsbScreen> {
String? _errorText;
Timer? _hotPlugTimer;
late final MeshCoreConnector _connector;
late final MeshCoreConnectorUsb _usbConnector;
late final VoidCallback _connectionListener;
/// Whether the current platform supports dynamic hot-plug polling.
@ -40,12 +43,13 @@ class _UsbScreenState extends State<UsbScreen> {
void initState() {
super.initState();
_connector = context.read<MeshCoreConnector>();
_usbConnector = MeshCoreConnectorUsb(_connector);
_connectionListener = () {
if (!mounted) return;
final activeUsbPortDisplayLabel = _connector.activeUsbPortDisplayLabel;
final activeUsbPortDisplayLabel = _usbConnector.activeUsbPortDisplayLabel;
final shouldUpdateDisplayLabel =
activeUsbPortDisplayLabel != _connectedPortDisplayLabel;
if (_connector.state == MeshCoreConnectionState.disconnected) {
if (_usbConnector.state == MeshCoreConnectionState.disconnected) {
_navigatedToContacts = false;
setState(() {
_isConnecting = false;
@ -56,8 +60,8 @@ class _UsbScreenState extends State<UsbScreen> {
_connectedPortDisplayLabel = activeUsbPortDisplayLabel;
});
}
if (_connector.state == MeshCoreConnectionState.connected &&
_connector.isUsbTransportConnected &&
if (_usbConnector.state == MeshCoreConnectionState.connected &&
_usbConnector.isUsbTransportConnected &&
!_navigatedToContacts) {
_navigatedToContacts = true;
Navigator.of(context).pushReplacement(
@ -65,14 +69,14 @@ class _UsbScreenState extends State<UsbScreen> {
);
}
};
_connector.addListener(_connectionListener);
_usbConnector.addListener(_connectionListener);
_startHotPlugTimer();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus);
_usbConnector.setRequestPortLabel(context.l10n.usbScreenStatus);
if (!_didScheduleInitialLoad) {
_didScheduleInitialLoad = true;
unawaited(_loadPorts());
@ -83,12 +87,12 @@ class _UsbScreenState extends State<UsbScreen> {
void dispose() {
_hotPlugTimer?.cancel();
_hotPlugTimer = null;
_connector.removeListener(_connectionListener);
_usbConnector.removeListener(_connectionListener);
if (!_navigatedToContacts &&
_connector.activeTransport == MeshCoreTransportType.usb &&
_connector.state != MeshCoreConnectionState.disconnected) {
_usbConnector.activeTransport == MeshCoreTransportType.usb &&
_usbConnector.state != MeshCoreConnectionState.disconnected) {
WidgetsBinding.instance.addPostFrameCallback((_) {
unawaited(_connector.disconnect(manual: true));
unawaited(_usbConnector.disconnect(manual: true));
});
}
super.dispose();
@ -113,6 +117,23 @@ class _UsbScreenState extends State<UsbScreen> {
style: theme.textTheme.titleLarge,
),
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(
child: LayoutBuilder(
@ -376,7 +397,8 @@ class _UsbScreenState extends State<UsbScreen> {
final isSelected = port == _selectedPort;
final displayName = _friendlyPortName(port);
final rawName = normalizeUsbPortName(port);
final showRawName = rawName != displayName;
final showRawName =
rawName != displayName && !rawName.startsWith('web:');
return Material(
color: isSelected
? theme.colorScheme.primaryContainer
@ -433,7 +455,7 @@ class _UsbScreenState extends State<UsbScreen> {
Future<void> _loadPorts() async {
if (!mounted) return;
_connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus);
_usbConnector.setRequestPortLabel(context.l10n.usbScreenStatus);
setState(() {
_isLoadingPorts = true;
@ -441,7 +463,7 @@ class _UsbScreenState extends State<UsbScreen> {
});
try {
final ports = await _connector.listUsbPorts();
final ports = await _usbConnector.listPorts();
if (!mounted) return;
setState(() {
_ports
@ -470,8 +492,8 @@ class _UsbScreenState extends State<UsbScreen> {
if (selectedPort == null || selectedPort.isEmpty) {
return;
}
_connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus);
if (_connector.state != MeshCoreConnectionState.disconnected) {
_usbConnector.setRequestPortLabel(context.l10n.usbScreenStatus);
if (_usbConnector.state != MeshCoreConnectionState.disconnected) {
setState(() {
_isConnecting = false;
_errorText = null;
@ -486,7 +508,7 @@ class _UsbScreenState extends State<UsbScreen> {
});
try {
await _connector.connectUsb(portName: rawPortName);
await _usbConnector.connect(portName: rawPortName);
} catch (error, stackTrace) {
debugPrint(
'UsbScreen: connect failed for $rawPortName: $error\n$stackTrace',

View file

@ -280,6 +280,11 @@ class UsbSerialService {
Future<void> disconnect() async {
if (_status == UsbSerialStatus.disconnected) return;
final portLabel = _connectedPortLabel ?? _connectedPortKey;
_debugLogService?.info(
'USB disconnect starting port=${portLabel ?? 'unknown'}',
tag: 'USB Serial',
);
_status = UsbSerialStatus.disconnecting;
_connectedPortKey = null;
_connectedPortLabel = null;
@ -319,6 +324,10 @@ class UsbSerialService {
_dataSubscription = null;
}
_status = UsbSerialStatus.disconnected;
_debugLogService?.info(
'USB disconnect complete port=${portLabel ?? 'unknown'}',
tag: 'USB Serial',
);
}
void setRequestPortLabel(String label) {

View file

@ -16,6 +16,10 @@ class UsbSerialService {
'2886:1667': 'Seeed Wio Tracker L1',
};
static final Map<String, String> _deviceNamesByPortKey = <String, String>{};
static final Map<String, String> _baseLabelsByPortKey = <String, String>{};
static final Map<String, JSObject> _authorizedPortsByKey =
<String, JSObject>{};
static int _nextAuthorizedPortId = 1;
final StreamController<Uint8List> _frameController =
StreamController<Uint8List>.broadcast();
@ -51,11 +55,12 @@ class UsbSerialService {
return const <String>[];
}
_resetPortCache();
final ports = await _getAuthorizedPorts();
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({
@ -75,8 +80,12 @@ class UsbSerialService {
try {
final requestedPortName = normalizeUsbPortName(portName);
final selectedPortKey = requestedPortName.startsWith('web:port:')
? requestedPortName
: null;
_port = _authorizedPortsByKey[requestedPortName];
final authorizedPorts = await _getAuthorizedPorts();
_port = _selectPort(authorizedPorts, requestedPortName);
_port ??= _selectPort(authorizedPorts, requestedPortName);
_port ??= await _requestPort();
if (_port == null) {
@ -84,8 +93,11 @@ class UsbSerialService {
}
await _openPort(_port!, baudRate);
_connectedPortKey = _portKeyFor(_port!);
_connectedPortName = _buildDisplayLabel(_connectedPortKey!);
_connectedPortKey = _cachePort(_port!, preferredKey: selectedPortKey);
_connectedPortName = _displayLabelForPort(
_port!,
portKey: _connectedPortKey,
);
_writer = _getWriter(_port!);
_reader = _getReader(_port!);
_status = UsbSerialStatus.connected;
@ -122,6 +134,11 @@ class UsbSerialService {
Future<void> disconnect() async {
if (_status == UsbSerialStatus.disconnected) return;
final portLabel = _connectedPortName ?? _connectedPortKey;
_debugLogService?.info(
'USB disconnect starting port=${portLabel ?? 'unknown'}',
tag: 'USB Serial',
);
_status = UsbSerialStatus.disconnecting;
final reader = _reader;
final writer = _writer;
@ -156,6 +173,10 @@ class UsbSerialService {
}
_status = UsbSerialStatus.disconnected;
_debugLogService?.info(
'USB disconnect complete port=${portLabel ?? 'unknown'}',
tag: 'USB Serial',
);
}
void updateConnectedLabel(String label) {
@ -210,9 +231,12 @@ class UsbSerialService {
if (ports.isEmpty) {
return null;
}
if (requestedPortName.isEmpty || requestedPortName == _requestPortLabel) {
if (requestedPortName.isEmpty || requestedPortName == _requestPortKey) {
return ports.first;
}
if (requestedPortName.startsWith('web:port:')) {
return null;
}
for (final port in ports) {
final description = _describePort(port);
if (description == requestedPortName) {
@ -368,10 +392,29 @@ class UsbSerialService {
}
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 {
final info = port.callMethod<JSAny?>('getInfo'.toJS);
if (info == null) {
return _requestPortLabel;
return null;
}
final infoObject = info as JSObject;
@ -381,32 +424,52 @@ class UsbSerialService {
final productId = infoObject
.getProperty<JSAny?>('usbProductId'.toJS)
?.dartify();
final hasVendor = vendorId is num;
final hasProduct = productId is num;
return describeWebUsbPort(
vendorId: hasVendor ? vendorId.toInt() : null,
productId: hasProduct ? productId.toInt() : null,
requestPortLabel: _requestPortLabel,
knownUsbNames: _knownUsbNames,
return _WebPortInfo(
usbVendorId: vendorId is num ? vendorId.toInt() : null,
usbProductId: productId is num ? productId.toInt() : null,
);
} catch (_) {
return _requestPortLabel;
return null;
}
}
String _portKeyFor(JSObject port) => _describePort(port);
String _portKeyFor(JSObject port) {
return _cachePort(port);
}
String _displayLabelForPort(JSObject port) =>
_buildDisplayLabel(_portKeyFor(port));
String _cachePort(JSObject port, {String? preferredKey}) {
final portKey = preferredKey ?? 'web:port:${_nextAuthorizedPortId++}';
_baseLabelsByPortKey[portKey] = _describePort(port);
_authorizedPortsByKey[portKey] = port;
return portKey;
}
String _displayLabelForPort(JSObject port, {String? portKey}) =>
_buildDisplayLabel(portKey ?? _portKeyFor(port));
String _buildDisplayLabel(String portKey) {
return buildUsbDisplayLabel(
basePortLabel: portKey,
basePortLabel: _baseLabelsByPortKey[portKey] ?? portKey,
deviceName: _deviceNamesByPortKey[portKey],
);
}
String _listEntryForPort(JSObject port) {
final portKey = _portKeyFor(port);
return '$portKey - ${_displayLabelForPort(port, portKey: portKey)}';
}
String get _requestPortKey => 'web:request';
String get _requestPortListEntry => '$_requestPortKey - $_requestPortLabel';
void _resetPortCache() {
_authorizedPortsByKey.clear();
_baseLabelsByPortKey.clear();
_deviceNamesByPortKey.clear();
_nextAuthorizedPortId = 1;
}
void _releaseLock(JSObject resource) {
try {
resource.callMethod<JSAny?>('releaseLock'.toJS);
@ -462,3 +525,10 @@ class UsbSerialService {
}
enum UsbSerialStatus { disconnected, connecting, connected, disconnecting }
final class _WebPortInfo {
const _WebPortInfo({required this.usbVendorId, required this.usbProductId});
final int? usbVendorId;
final int? usbProductId;
}

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import 'app_logger.dart';
/// Shows a confirmation dialog before disconnecting from the device.
/// Returns true if user confirmed and disconnect completed, false otherwise.
@ -28,6 +29,7 @@ Future<bool> showDisconnectDialog(
);
if (confirmed == true) {
appLogger.info('Disconnect confirmed from popup', tag: 'Connection');
await connector.disconnect();
return true;
}

View file

@ -4,7 +4,7 @@ import 'package:provider/provider.dart';
import 'package:meshcore_open/connector/meshcore_connector.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/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,
) async {
final connector = _FakeMeshCoreConnector();
@ -139,19 +139,15 @@ void main() {
await tester.pumpWidget(
_buildTestApp(
connector: connector,
child: const ConnectionChoiceScreen(),
child: const ScannerScreen(),
),
);
await tester.pumpAndSettle();
final usbButton = tester.widget<ElevatedButton>(
find.widgetWithText(ElevatedButton, 'USB'),
);
if (PlatformInfo.supportsUsbSerial) {
expect(usbButton.onPressed, isNotNull);
expect(find.widgetWithText(FloatingActionButton, 'USB'), findsOneWidget);
} else {
expect(usbButton.onPressed, isNull);
expect(find.widgetWithText(FloatingActionButton, 'USB'), findsNothing);
}
});
}