mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-08-05 23:42:57 +10:00
Compare commits
223 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fef73b7b62 | |||
| 8238b6197f | |||
| 435ba89982 | |||
| 0565cee461 | |||
| ab2b509d6a | |||
| eba95af31f | |||
| 04c016cfe1 | |||
| ea2354712d | |||
| 7a0b8aad3d | |||
| bd34bb5e88 | |||
| 81548fdc21 | |||
| b2770ef028 | |||
| 7c479f9121 | |||
| 1f2dfc555b | |||
| 8eb6f32fef | |||
| d96cd34771 | |||
| fb58a3262c | |||
| f584c4fba0 | |||
| b5b930646f | |||
| 3452bdae8c | |||
| 25fc9454a8 | |||
| 524558c511 | |||
| 367e47bb1e | |||
| 21ff765e41 | |||
| 38d40ca0a4 | |||
| 5b4535d5dc | |||
| f9b6299620 | |||
| 7cb84dbf6f | |||
| 44c0670dae | |||
| 74da9e82b5 | |||
| 63583dadda | |||
| 32632669c3 | |||
| 3c0c0d1dea | |||
| e6c9a3fea7 | |||
| f5154b0033 | |||
| 4c7ee3b3b0 | |||
| c2f544eeba | |||
| 98cdac4309 | |||
| d6d11eaad2 | |||
| 3cef9e81b6 | |||
| 5216e00807 | |||
| a0feb129e1 | |||
| f39a22668e | |||
| 781090243c | |||
| ca5784f3f8 | |||
| dcad5c586d | |||
| 4b24506310 | |||
| 47c4e0fb82 | |||
| c041e05972 | |||
| 612612795a | |||
| 3cec3dc233 | |||
| 3542adad1d | |||
| 115689ad95 | |||
| 9a0572e8e4 | |||
| 2d1160d992 | |||
| ee3af52c0f | |||
| 98f7c3b088 | |||
| f462815775 | |||
| 5f4333398e | |||
| c23a1da430 | |||
| 22a53439b1 | |||
| 7d8e049745 | |||
| 3502559fae | |||
| e125318137 | |||
| d53465d13b | |||
| a0efbbe4bd | |||
| bd5db9a9d5 | |||
| 79b17b53a0 | |||
| 647fe1523e | |||
| b7d5ee5754 | |||
| 38856c67e5 | |||
| 6bd3c17cdf | |||
| 6d0712c450 | |||
| ddeb1edc2e | |||
| 8d73602509 | |||
| fcab69f9f0 | |||
| d2640e1294 | |||
| b02225c02e | |||
| 128e99e3e7 | |||
| 12bf46bba1 | |||
| 92d8e7cd0b | |||
| 75610695c2 | |||
| 57ea30cae9 | |||
| e139383335 | |||
| 64428294c9 | |||
| e7a8c36bc4 | |||
| 2a62390903 | |||
| 75d25f6312 | |||
| 2a3119544c | |||
| fb41a5bf10 | |||
| d88786bb0f | |||
| e3148dd449 | |||
| 96371c03ae | |||
| cac65face6 | |||
| bdb1eb6b42 | |||
| f2ccec2926 | |||
| 31671958d5 | |||
| ea379ce50b | |||
| 50af2e0bc9 | |||
| d5ac84430c | |||
| 190fd3b353 | |||
| a2d1cb2a99 | |||
| 83386a8cde | |||
| acc0fff2dc | |||
| a26055c93f | |||
| 5a70ed48cf | |||
| a777236cd9 | |||
| a42cf77a70 | |||
| 31db565ebf | |||
| 515b9c1f29 | |||
| ea1d728d4f | |||
| 86bde1d178 | |||
| de63733bb9 | |||
| c880c2d107 | |||
| 2a7cc28a3a | |||
| 8a16024642 | |||
| 0f17e2382c | |||
| 6065059241 | |||
| faefef14ff | |||
| ddc87f3a27 | |||
| 2188b49726 | |||
| 1a9b7b0d55 | |||
| 74e29a6c0f | |||
| 7740698cde | |||
| 972ae809e3 | |||
| deb46553f3 | |||
| 58fc55df13 | |||
| ea2f35ec2e | |||
| e2585c0992 | |||
| 78f1a7b28e | |||
| 0121b5f649 | |||
| ec14870aed | |||
| c0516a475d | |||
| b998186430 | |||
| 16b2c24983 | |||
| c8ff0cc943 | |||
| 64bf307d09 | |||
| 88f8066ed3 | |||
| c8f93f9902 | |||
| c34be44950 | |||
| bf5fadd15e | |||
| 3730b2a6c2 | |||
| 173fdf7168 | |||
| 549fc62632 | |||
| 53d073d8f2 | |||
| 7465e81996 | |||
| 677b25908a | |||
| fc55fb98ce | |||
| 2bdd9d35cc | |||
| 1f816f7e08 | |||
| bd27c90216 | |||
| 9bcb8b9ca6 | |||
| aaf79c90c9 | |||
| 08edd2696e | |||
| 0f2d18d6fa | |||
| 298951f8bc | |||
| f3db63ceea | |||
| 47044ae14e | |||
| f4dd76a459 | |||
| ab76a52d71 | |||
| 332bb5ef3a | |||
| 81a423d096 | |||
| 700e85b13d | |||
| 9a27953a6e | |||
| abde4a5e46 | |||
| 6e1cb0482f | |||
| c28b38a233 | |||
| 722caf774e | |||
| 4975b5366e | |||
| d269e181c3 | |||
| 35498c1b90 | |||
| bf4f52a4e3 | |||
| c284e571b0 | |||
| a1ee0789a6 | |||
| 3ca53e967c | |||
| 096e0a4184 | |||
| 40ac95e8e6 | |||
| 377f1df445 | |||
| 9865a03c53 | |||
| a5555bd606 | |||
| 1b4d31a36e | |||
| 8e07440114 | |||
| 71129bdf4d | |||
| ab05cf8b3e | |||
| 452e5337f0 | |||
| 6ac987e7cf | |||
| 5522f9a236 | |||
| b4f79c1aae | |||
| b08defcff4 | |||
| 5676cbd84e | |||
| cf8f01128b | |||
| b5e47ce44f | |||
| 7b2f75047c | |||
| 6d63e49938 | |||
| c7b33f1d1b | |||
| 7288f11c88 | |||
| 2306269384 | |||
| 41ff2353a4 | |||
| b3ad54f296 | |||
| 7cb4c5a334 | |||
| bb8ad70cb9 | |||
| 8fe4129204 | |||
| 2feff809ff | |||
| 51d70ce086 | |||
| b05b62eeee | |||
| 061b715694 | |||
| f38b8b0319 | |||
| 304c389669 | |||
| 7acfe47fd7 | |||
| f4b18d97a1 | |||
| d2b693e5ce | |||
| ba2763a3f6 | |||
| 0c4910e149 | |||
| 4bf2519559 | |||
| 19edeab9d5 | |||
| 0e81d75cce | |||
| 9437846127 | |||
| 50ab46ed40 | |||
| dc193be8ed | |||
| 8a804a3706 | |||
| 1dc90d0e89 | |||
| 5f2312e086 | |||
| a9fbf8c7f5 |
@@ -0,0 +1,38 @@
|
|||||||
|
name: Deploy to Cloudflare Workers
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: 'stable'
|
||||||
|
# Match local development version which provides Dart 3.11.0
|
||||||
|
flutter-version: '3.41.2'
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
|
||||||
|
- name: Get dependencies
|
||||||
|
run: flutter pub get
|
||||||
|
|
||||||
|
- name: Build Web
|
||||||
|
run: bun run build
|
||||||
|
|
||||||
|
- name: Deploy to Cloudflare
|
||||||
|
uses: cloudflare/wrangler-action@v3
|
||||||
|
with:
|
||||||
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
command: deploy
|
||||||
@@ -30,6 +30,7 @@ migrate_working_dir/
|
|||||||
.flutter-plugins-dependencies
|
.flutter-plugins-dependencies
|
||||||
.pub-cache/
|
.pub-cache/
|
||||||
.pub/
|
.pub/
|
||||||
|
pubspec.lock
|
||||||
/build/
|
/build/
|
||||||
/coverage/
|
/coverage/
|
||||||
|
|
||||||
@@ -82,3 +83,6 @@ keystore.properties
|
|||||||
# IDE
|
# IDE
|
||||||
.vscode/launch.json
|
.vscode/launch.json
|
||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
|
|
||||||
|
# Cloudflare Wrangler
|
||||||
|
.wrangler
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
4.0.0
|
||||||
@@ -78,6 +78,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
|||||||
- ✅ **Android**: Full support (API 21+)
|
- ✅ **Android**: Full support (API 21+)
|
||||||
- ✅ **iOS**: Full support (iOS 12+)
|
- ✅ **iOS**: Full support (iOS 12+)
|
||||||
- 🚧 **Desktop**: Limited support (macOS/Linux/Windows)
|
- 🚧 **Desktop**: Limited support (macOS/Linux/Windows)
|
||||||
|
- 🚧 **Web**: Under construction (Chrome)
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
@@ -230,6 +231,11 @@ If you find MeshCore Open useful and would like to support development, you can
|
|||||||
|
|
||||||
**Solana Address:** `F15YanjZj96YTBtKJYgNa8RLQLCZkx5CEwogPWkqXeoQ`
|
**Solana Address:** `F15YanjZj96YTBtKJYgNa8RLQLCZkx5CEwogPWkqXeoQ`
|
||||||
|
|
||||||
|
|
||||||
|
**Monero Address:** `453TxnpUqjkJtXxzdjMsrgERNkBRXEGamPbpC45ENrvKAk9tH7kZbxWF82Hz66etgDZyXFPEBU2JUEqhLeJyWt9kBvTVy5m`
|
||||||
|
|
||||||
|
**Bitcoin Address:** `bc1qh45x28v8dslcg4v4upmqd9g0mvc3lnyffmyzr5`
|
||||||
|
|
||||||
Your support helps maintain and improve this open-source project!
|
Your support helps maintain and improve this open-source project!
|
||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ android {
|
|||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
isCoreLibraryDesugaringEnabled = true
|
isCoreLibraryDesugaringEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
<!-- Camera permission for QR code scanning -->
|
<!-- Camera permission for QR code scanning -->
|
||||||
<uses-permission android:name="android.permission.CAMERA"/>
|
<uses-permission android:name="android.permission.CAMERA"/>
|
||||||
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
||||||
|
<uses-feature android:name="android.hardware.usb.host" android:required="false"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="meshcore_open"
|
android:label="meshcore_open"
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
package com.meshcore.meshcore_open
|
package com.meshcore.meshcore_open
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
|
||||||
class MainActivity : FlutterActivity()
|
class MainActivity : FlutterActivity() {
|
||||||
|
private val usbFunctions by lazy { MeshcoreUsbFunctions(this) }
|
||||||
|
|
||||||
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
|
super.configureFlutterEngine(flutterEngine)
|
||||||
|
usbFunctions.configureFlutterEngine(flutterEngine)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
usbFunctions.dispose()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,582 @@
|
|||||||
|
package com.meshcore.meshcore_open
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.hardware.usb.UsbConstants
|
||||||
|
import android.hardware.usb.UsbDevice
|
||||||
|
import android.hardware.usb.UsbDeviceConnection
|
||||||
|
import android.hardware.usb.UsbEndpoint
|
||||||
|
import android.hardware.usb.UsbInterface
|
||||||
|
import android.hardware.usb.UsbManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
import io.flutter.plugin.common.EventChannel
|
||||||
|
import io.flutter.plugin.common.MethodCall
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.ExecutorService
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
class MeshcoreUsbFunctions(
|
||||||
|
private val activity: FlutterActivity,
|
||||||
|
) {
|
||||||
|
private companion object {
|
||||||
|
const val usbRecipientInterface = 0x01
|
||||||
|
}
|
||||||
|
|
||||||
|
private val usbMethodChannelName = "meshcore_open/android_usb_serial"
|
||||||
|
private val usbEventChannelName = "meshcore_open/android_usb_serial_events"
|
||||||
|
private val usbPermissionAction = "com.meshcore.meshcore_open.USB_PERMISSION"
|
||||||
|
|
||||||
|
private val usbManager by lazy {
|
||||||
|
activity.getSystemService(Context.USB_SERVICE) as UsbManager
|
||||||
|
}
|
||||||
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
|
private val usbIoExecutor: ExecutorService = Executors.newSingleThreadExecutor()
|
||||||
|
|
||||||
|
@Volatile private var eventSink: EventChannel.EventSink? = null
|
||||||
|
@Volatile private var usbConnection: UsbDeviceConnection? = null
|
||||||
|
@Volatile private var usbInEndpoint: UsbEndpoint? = null
|
||||||
|
@Volatile private var usbOutEndpoint: UsbEndpoint? = null
|
||||||
|
@Volatile private var controlInterface: UsbInterface? = null
|
||||||
|
@Volatile private var dataInterface: UsbInterface? = null
|
||||||
|
private var readThread: Thread? = null
|
||||||
|
@Volatile private var isReading = false
|
||||||
|
@Volatile private var connectedDeviceName: String? = null
|
||||||
|
|
||||||
|
private var pendingConnectResult: MethodChannel.Result? = null
|
||||||
|
private var pendingConnectPortName: String? = null
|
||||||
|
private var pendingConnectBaudRate: Int = 115200
|
||||||
|
|
||||||
|
private data class PortConfig(
|
||||||
|
val controlInterface: UsbInterface?,
|
||||||
|
val dataInterface: UsbInterface,
|
||||||
|
val inEndpoint: UsbEndpoint,
|
||||||
|
val outEndpoint: UsbEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val permissionReceiver =
|
||||||
|
object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
when (intent?.action) {
|
||||||
|
UsbManager.ACTION_USB_DEVICE_DETACHED -> {
|
||||||
|
handleUsbDetached(intent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
usbPermissionAction -> Unit
|
||||||
|
else -> return
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = pendingConnectResult
|
||||||
|
val portName = pendingConnectPortName
|
||||||
|
pendingConnectResult = null
|
||||||
|
pendingConnectPortName = null
|
||||||
|
|
||||||
|
if (result == null || portName == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val device = findUsbDevice(portName)
|
||||||
|
if (device == null) {
|
||||||
|
result.error(
|
||||||
|
"usb_device_missing",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val granted =
|
||||||
|
intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
|
||||||
|
if (!granted || !usbManager.hasPermission(device)) {
|
||||||
|
result.error("usb_permission_denied", null, null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
openUsbDevice(device, pendingConnectBaudRate, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
|
registerUsbPermissionReceiver()
|
||||||
|
|
||||||
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, usbMethodChannelName)
|
||||||
|
.setMethodCallHandler { call, result ->
|
||||||
|
when (call.method) {
|
||||||
|
"listPorts" -> result.success(listUsbPorts())
|
||||||
|
"connect" -> handleUsbConnect(call, result)
|
||||||
|
"write" -> handleUsbWrite(call, result)
|
||||||
|
"disconnect" -> {
|
||||||
|
scheduleCloseUsbConnection {
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> result.notImplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EventChannel(flutterEngine.dartExecutor.binaryMessenger, usbEventChannelName)
|
||||||
|
.setStreamHandler(
|
||||||
|
object : EventChannel.StreamHandler {
|
||||||
|
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
|
||||||
|
eventSink = events
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCancel(arguments: Any?) {
|
||||||
|
eventSink = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dispose() {
|
||||||
|
closeUsbConnection()
|
||||||
|
usbIoExecutor.shutdownNow()
|
||||||
|
try {
|
||||||
|
activity.unregisterReceiver(permissionReceiver)
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun registerUsbPermissionReceiver() {
|
||||||
|
val filter =
|
||||||
|
IntentFilter().apply {
|
||||||
|
addAction(usbPermissionAction)
|
||||||
|
addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
activity.registerReceiver(permissionReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
activity.registerReceiver(permissionReceiver, filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun listUsbPorts(): List<String> {
|
||||||
|
return usbManager.deviceList.values.map { device ->
|
||||||
|
val productName = device.productName ?: "USB Serial Device"
|
||||||
|
val vendorProduct =
|
||||||
|
String.format(
|
||||||
|
Locale.US,
|
||||||
|
"VID:%04X PID:%04X",
|
||||||
|
device.vendorId,
|
||||||
|
device.productId,
|
||||||
|
)
|
||||||
|
"${device.deviceName} - $productName - $vendorProduct"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUsbConnect(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val portName = call.argument<String>("portName")
|
||||||
|
val baudRate = call.argument<Int>("baudRate") ?: 115200
|
||||||
|
if (portName.isNullOrBlank()) {
|
||||||
|
result.error("usb_invalid_port", null, null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val device = findUsbDevice(portName)
|
||||||
|
if (device == null) {
|
||||||
|
result.error("usb_device_missing", null, null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usbManager.hasPermission(device)) {
|
||||||
|
openUsbDevice(device, baudRate, result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingConnectResult != null) {
|
||||||
|
result.error("usb_busy", null, null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingConnectResult = result
|
||||||
|
pendingConnectPortName = portName
|
||||||
|
pendingConnectBaudRate = baudRate
|
||||||
|
|
||||||
|
val permissionIntent = PendingIntent.getBroadcast(
|
||||||
|
activity,
|
||||||
|
0,
|
||||||
|
Intent(usbPermissionAction).setPackage(activity.packageName),
|
||||||
|
pendingIntentFlags(),
|
||||||
|
)
|
||||||
|
usbManager.requestPermission(device, permissionIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUsbWrite(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val data = call.argument<ByteArray>("data")
|
||||||
|
val connection = usbConnection
|
||||||
|
val endpoint = usbOutEndpoint
|
||||||
|
if (data == null) {
|
||||||
|
result.error("usb_invalid_data", null, null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (connection == null || endpoint == null) {
|
||||||
|
result.error("usb_not_connected", null, null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
usbIoExecutor.execute {
|
||||||
|
try {
|
||||||
|
writeToDevice(data)
|
||||||
|
mainHandler.post { result.success(null) }
|
||||||
|
} catch (error: Exception) {
|
||||||
|
mainHandler.post {
|
||||||
|
result.error("usb_write_failed", error.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findUsbDevice(portName: String): UsbDevice? {
|
||||||
|
val devices = usbManager.deviceList.values
|
||||||
|
val exactMatch = devices.firstOrNull { it.deviceName == portName }
|
||||||
|
if (exactMatch != null) {
|
||||||
|
return exactMatch
|
||||||
|
}
|
||||||
|
|
||||||
|
val normalizedName = portName.substringBefore(" - ").trim()
|
||||||
|
return devices.firstOrNull { it.deviceName == normalizedName }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openUsbDevice(
|
||||||
|
device: UsbDevice,
|
||||||
|
baudRate: Int,
|
||||||
|
result: MethodChannel.Result,
|
||||||
|
) {
|
||||||
|
usbIoExecutor.execute {
|
||||||
|
try {
|
||||||
|
closeUsbConnection()
|
||||||
|
|
||||||
|
val config = resolvePortConfig(device)
|
||||||
|
if (config == null) {
|
||||||
|
mainHandler.post {
|
||||||
|
result.error(
|
||||||
|
"usb_driver_missing",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return@execute
|
||||||
|
}
|
||||||
|
|
||||||
|
val connection = usbManager.openDevice(device)
|
||||||
|
if (connection == null) {
|
||||||
|
mainHandler.post {
|
||||||
|
result.error(
|
||||||
|
"usb_open_failed",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return@execute
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!connection.claimInterface(config.dataInterface, true)) {
|
||||||
|
connection.close()
|
||||||
|
mainHandler.post {
|
||||||
|
result.error(
|
||||||
|
"usb_open_failed",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return@execute
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.controlInterface != null &&
|
||||||
|
config.controlInterface.id != config.dataInterface.id &&
|
||||||
|
!connection.claimInterface(config.controlInterface, true)
|
||||||
|
) {
|
||||||
|
connection.releaseInterface(config.dataInterface)
|
||||||
|
connection.close()
|
||||||
|
mainHandler.post {
|
||||||
|
result.error(
|
||||||
|
"usb_open_failed",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return@execute
|
||||||
|
}
|
||||||
|
|
||||||
|
usbConnection = connection
|
||||||
|
usbInEndpoint = config.inEndpoint
|
||||||
|
usbOutEndpoint = config.outEndpoint
|
||||||
|
controlInterface = config.controlInterface
|
||||||
|
dataInterface = config.dataInterface
|
||||||
|
|
||||||
|
configureDevice(connection, config, baudRate)
|
||||||
|
|
||||||
|
connectedDeviceName = device.deviceName
|
||||||
|
startReadLoop()
|
||||||
|
|
||||||
|
mainHandler.post {
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
|
} catch (error: Exception) {
|
||||||
|
closeUsbConnection()
|
||||||
|
mainHandler.post {
|
||||||
|
result.error("usb_connect_failed", error.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolvePortConfig(device: UsbDevice): PortConfig? {
|
||||||
|
var preferredDataInterface: UsbInterface? = null
|
||||||
|
var preferredInEndpoint: UsbEndpoint? = null
|
||||||
|
var preferredOutEndpoint: UsbEndpoint? = null
|
||||||
|
var fallbackDataInterface: UsbInterface? = null
|
||||||
|
var fallbackInEndpoint: UsbEndpoint? = null
|
||||||
|
var fallbackOutEndpoint: UsbEndpoint? = null
|
||||||
|
var preferredControlInterface: UsbInterface? = null
|
||||||
|
|
||||||
|
for (interfaceIndex in 0 until device.interfaceCount) {
|
||||||
|
val usbInterface = device.getInterface(interfaceIndex)
|
||||||
|
var inEndpoint: UsbEndpoint? = null
|
||||||
|
var outEndpoint: UsbEndpoint? = null
|
||||||
|
|
||||||
|
for (endpointIndex in 0 until usbInterface.endpointCount) {
|
||||||
|
val endpoint = usbInterface.getEndpoint(endpointIndex)
|
||||||
|
if (endpoint.type != UsbConstants.USB_ENDPOINT_XFER_BULK) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
when (endpoint.direction) {
|
||||||
|
UsbConstants.USB_DIR_IN -> if (inEndpoint == null) inEndpoint = endpoint
|
||||||
|
UsbConstants.USB_DIR_OUT -> if (outEndpoint == null) outEndpoint = endpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasDataPair = inEndpoint != null && outEndpoint != null
|
||||||
|
when {
|
||||||
|
usbInterface.interfaceClass == UsbConstants.USB_CLASS_COMM &&
|
||||||
|
preferredControlInterface == null -> {
|
||||||
|
preferredControlInterface = usbInterface
|
||||||
|
}
|
||||||
|
hasDataPair &&
|
||||||
|
usbInterface.interfaceClass == UsbConstants.USB_CLASS_CDC_DATA -> {
|
||||||
|
preferredDataInterface = usbInterface
|
||||||
|
preferredInEndpoint = inEndpoint
|
||||||
|
preferredOutEndpoint = outEndpoint
|
||||||
|
}
|
||||||
|
hasDataPair && fallbackDataInterface == null -> {
|
||||||
|
fallbackDataInterface = usbInterface
|
||||||
|
fallbackInEndpoint = inEndpoint
|
||||||
|
fallbackOutEndpoint = outEndpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val dataInterface = preferredDataInterface ?: fallbackDataInterface ?: return null
|
||||||
|
val inEndpoint = preferredInEndpoint ?: fallbackInEndpoint ?: return null
|
||||||
|
val outEndpoint = preferredOutEndpoint ?: fallbackOutEndpoint ?: return null
|
||||||
|
return PortConfig(preferredControlInterface, dataInterface, inEndpoint, outEndpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun configureDevice(
|
||||||
|
connection: UsbDeviceConnection,
|
||||||
|
config: PortConfig,
|
||||||
|
baudRate: Int,
|
||||||
|
) {
|
||||||
|
val control = config.controlInterface ?: return
|
||||||
|
val lineCoding =
|
||||||
|
byteArrayOf(
|
||||||
|
(baudRate and 0xFF).toByte(),
|
||||||
|
((baudRate shr 8) and 0xFF).toByte(),
|
||||||
|
((baudRate shr 16) and 0xFF).toByte(),
|
||||||
|
((baudRate shr 24) and 0xFF).toByte(),
|
||||||
|
0, // stop bits: 1
|
||||||
|
0, // parity: none
|
||||||
|
8, // data bits
|
||||||
|
)
|
||||||
|
|
||||||
|
val lineCodingResult =
|
||||||
|
connection.controlTransfer(
|
||||||
|
UsbConstants.USB_DIR_OUT or
|
||||||
|
UsbConstants.USB_TYPE_CLASS or
|
||||||
|
usbRecipientInterface,
|
||||||
|
0x20,
|
||||||
|
0,
|
||||||
|
control.id,
|
||||||
|
lineCoding,
|
||||||
|
lineCoding.size,
|
||||||
|
1000,
|
||||||
|
)
|
||||||
|
if (lineCodingResult < 0) {
|
||||||
|
throw IllegalStateException("Failed to configure USB line coding")
|
||||||
|
}
|
||||||
|
|
||||||
|
val controlLineResult =
|
||||||
|
connection.controlTransfer(
|
||||||
|
UsbConstants.USB_DIR_OUT or
|
||||||
|
UsbConstants.USB_TYPE_CLASS or
|
||||||
|
usbRecipientInterface,
|
||||||
|
0x22,
|
||||||
|
0x0001, // DTR on, RTS off
|
||||||
|
control.id,
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
1000,
|
||||||
|
)
|
||||||
|
if (controlLineResult < 0) {
|
||||||
|
throw IllegalStateException("Failed to configure USB control line state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startReadLoop() {
|
||||||
|
val connection = usbConnection ?: return
|
||||||
|
val endpoint = usbInEndpoint ?: return
|
||||||
|
|
||||||
|
isReading = true
|
||||||
|
readThread =
|
||||||
|
Thread({
|
||||||
|
val packetSize = endpoint.maxPacketSize.coerceAtLeast(64)
|
||||||
|
val buffer = ByteArray(packetSize * 4)
|
||||||
|
try {
|
||||||
|
while (isReading) {
|
||||||
|
val bytesRead = connection.bulkTransfer(endpoint, buffer, buffer.size, 250)
|
||||||
|
if (!isReading) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (bytesRead <= 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val packet = buffer.copyOf(bytesRead)
|
||||||
|
mainHandler.post {
|
||||||
|
eventSink?.success(packet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: Exception) {
|
||||||
|
if (isReading) {
|
||||||
|
mainHandler.post {
|
||||||
|
eventSink?.error(
|
||||||
|
"usb_io_error",
|
||||||
|
error.message ?: "USB serial I/O error",
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
scheduleCloseUsbConnection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, "MeshCoreUsbRead").also { thread ->
|
||||||
|
thread.isDaemon = true
|
||||||
|
thread.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun writeToDevice(data: ByteArray) {
|
||||||
|
val connection = usbConnection ?: throw IllegalStateException("USB connection missing")
|
||||||
|
val endpoint = usbOutEndpoint ?: throw IllegalStateException("USB output endpoint missing")
|
||||||
|
var offset = 0
|
||||||
|
val maxPacketSize = endpoint.maxPacketSize.coerceAtLeast(64)
|
||||||
|
while (offset < data.size) {
|
||||||
|
val chunkSize = minOf(maxPacketSize, data.size - offset)
|
||||||
|
val chunk = data.copyOfRange(offset, offset + chunkSize)
|
||||||
|
val bytesWritten = connection.bulkTransfer(endpoint, chunk, chunkSize, 1000)
|
||||||
|
if (bytesWritten != chunkSize) {
|
||||||
|
throw IllegalStateException("Short USB write: wrote $bytesWritten of $chunkSize bytes")
|
||||||
|
}
|
||||||
|
offset += chunkSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scheduleCloseUsbConnection(onComplete: (() -> Unit)? = null) {
|
||||||
|
usbIoExecutor.execute {
|
||||||
|
closeUsbConnection()
|
||||||
|
if (onComplete != null) {
|
||||||
|
mainHandler.post(onComplete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun closeUsbConnection() {
|
||||||
|
isReading = false
|
||||||
|
readThread?.interrupt()
|
||||||
|
if (readThread != null && readThread !== Thread.currentThread()) {
|
||||||
|
try {
|
||||||
|
readThread?.join(300)
|
||||||
|
} catch (_: InterruptedException) {
|
||||||
|
Thread.currentThread().interrupt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
readThread = null
|
||||||
|
|
||||||
|
val connection = usbConnection
|
||||||
|
val claimedControl = controlInterface
|
||||||
|
val claimedData = dataInterface
|
||||||
|
|
||||||
|
usbInEndpoint = null
|
||||||
|
usbOutEndpoint = null
|
||||||
|
controlInterface = null
|
||||||
|
dataInterface = null
|
||||||
|
usbConnection = null
|
||||||
|
|
||||||
|
if (connection != null) {
|
||||||
|
if (claimedControl != null) {
|
||||||
|
try {
|
||||||
|
connection.releaseInterface(claimedControl)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (claimedData != null && claimedData.id != claimedControl?.id) {
|
||||||
|
try {
|
||||||
|
connection.releaseInterface(claimedData)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
connection.close()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connectedDeviceName = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUsbDetached(intent: Intent) {
|
||||||
|
val detachedDevice =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)
|
||||||
|
}
|
||||||
|
|
||||||
|
val detachedName = detachedDevice?.deviceName ?: return
|
||||||
|
|
||||||
|
if (pendingConnectPortName == detachedName) {
|
||||||
|
pendingConnectResult?.error(
|
||||||
|
"usb_device_detached",
|
||||||
|
"USB device was removed before the connection completed",
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
pendingConnectResult = null
|
||||||
|
pendingConnectPortName = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectedDeviceName == detachedName) {
|
||||||
|
scheduleCloseUsbConnection {
|
||||||
|
eventSink?.error(
|
||||||
|
"usb_device_detached",
|
||||||
|
"USB device was disconnected",
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pendingIntentFlags(): Int {
|
||||||
|
var flags = PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
flags = flags or PendingIntent.FLAG_MUTABLE
|
||||||
|
}
|
||||||
|
return flags
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M268-240 42-466l57-56 170 170 56 56-57 56Zm226 0L268-466l56-57 170 170 368-368 56 57-424 424Zm0-226-57-56 198-198 57 56-198 198Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 253 B |
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
+1533
-200
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import '../services/app_debug_log_service.dart';
|
||||||
|
import '../services/usb_serial_service.dart';
|
||||||
|
|
||||||
|
/// Manages USB serial transport for MeshCore devices.
|
||||||
|
///
|
||||||
|
/// Owns the [UsbSerialService] and USB-specific connection state.
|
||||||
|
/// The main [MeshCoreConnector] delegates all USB operations here.
|
||||||
|
class MeshCoreUsbManager {
|
||||||
|
MeshCoreUsbManager();
|
||||||
|
|
||||||
|
final UsbSerialService _service = UsbSerialService();
|
||||||
|
AppDebugLogService? _debugLog;
|
||||||
|
String? _activePortKey;
|
||||||
|
String? _activePortLabel;
|
||||||
|
|
||||||
|
// --- Getters ---
|
||||||
|
String? get activePortKey => _activePortKey;
|
||||||
|
String? get activePortDisplayLabel => _activePortLabel ?? _activePortKey;
|
||||||
|
bool get isConnected => _service.isConnected;
|
||||||
|
Stream<Uint8List> get frameStream => _service.frameStream;
|
||||||
|
|
||||||
|
// --- Configuration ---
|
||||||
|
Future<List<String>> listPorts() => _service.listPorts();
|
||||||
|
|
||||||
|
void setRequestPortLabel(String label) =>
|
||||||
|
_service.setRequestPortLabel(label);
|
||||||
|
|
||||||
|
void setFallbackDeviceName(String label) =>
|
||||||
|
_service.setFallbackDeviceName(label);
|
||||||
|
|
||||||
|
void setDebugLogService(AppDebugLogService? service) {
|
||||||
|
_debugLog = service;
|
||||||
|
_service.setDebugLogService(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Connection lifecycle ---
|
||||||
|
Future<void> connect({required String portName, int baudRate = 115200}) async {
|
||||||
|
_debugLog?.info(
|
||||||
|
'UsbManager.connect: portName=$portName baud=$baudRate',
|
||||||
|
tag: 'USB',
|
||||||
|
);
|
||||||
|
await _service.connect(portName: portName, baudRate: baudRate);
|
||||||
|
_activePortKey = _service.activePortKey ?? portName;
|
||||||
|
_activePortLabel = _service.activePortDisplayLabel ?? portName;
|
||||||
|
_debugLog?.info(
|
||||||
|
'UsbManager.connect: done, key=$_activePortKey label=$_activePortLabel',
|
||||||
|
tag: 'USB',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> disconnect() async {
|
||||||
|
_debugLog?.info('UsbManager.disconnect', tag: 'USB');
|
||||||
|
await _service.disconnect();
|
||||||
|
_activePortKey = null;
|
||||||
|
_activePortLabel = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> write(Uint8List data) => _service.write(data);
|
||||||
|
|
||||||
|
// --- Label management ---
|
||||||
|
void updateConnectedLabel(String selfName) {
|
||||||
|
_service.updateConnectedLabel(selfName);
|
||||||
|
_activePortLabel = _service.activePortDisplayLabel ?? _activePortLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_service.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,19 +13,35 @@ class BufferReader {
|
|||||||
int readByte() => readBytes(1)[0];
|
int readByte() => readBytes(1)[0];
|
||||||
|
|
||||||
Uint8List readBytes(int count) {
|
Uint8List readBytes(int count) {
|
||||||
|
if (_pointer + count > _buffer.length) {
|
||||||
|
throw RangeError(
|
||||||
|
'Attempted to read $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
|
||||||
|
);
|
||||||
|
}
|
||||||
final data = _buffer.sublist(_pointer, _pointer + count);
|
final data = _buffer.sublist(_pointer, _pointer + count);
|
||||||
_pointer += count;
|
_pointer += count;
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
void skipBytes(int count) {
|
void skipBytes(int count) {
|
||||||
|
if (_pointer + count > _buffer.length) {
|
||||||
|
throw RangeError(
|
||||||
|
'Attempted to skip $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
|
||||||
|
);
|
||||||
|
}
|
||||||
_pointer += count;
|
_pointer += count;
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint8List readRemainingBytes() => readBytes(remaining);
|
Uint8List readRemainingBytes() => readBytes(remaining);
|
||||||
|
|
||||||
String readString() =>
|
String readString() {
|
||||||
utf8.decode(readRemainingBytes(), allowMalformed: true);
|
final value = readRemainingBytes();
|
||||||
|
try {
|
||||||
|
return utf8.decode(Uint8List.fromList(value), allowMalformed: true);
|
||||||
|
} catch (e) {
|
||||||
|
return String.fromCharCodes(value); // Latin-1 fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String readCString(int maxLength) {
|
String readCString(int maxLength) {
|
||||||
final value = <int>[];
|
final value = <int>[];
|
||||||
@@ -104,25 +120,27 @@ class BufferWriter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void writeHex(String hex) {
|
void writeHex(String hex) {
|
||||||
// Validate hex string length is even and not empty
|
writeBytes(hex2Uint8List(hex));
|
||||||
if (hex.isEmpty || hex.length % 2 != 0) {
|
|
||||||
throw FormatException('Invalid hex string length: ${hex.length}');
|
|
||||||
}
|
|
||||||
List<int> result = [];
|
|
||||||
for (int i = 0; i < hex.length ~/ 2; i++) {
|
|
||||||
final hexByte = hex.substring(i * 2, i * 2 + 2);
|
|
||||||
final byte = int.tryParse(hexByte, radix: 16);
|
|
||||||
if (byte == null) {
|
|
||||||
throw FormatException(
|
|
||||||
'Invalid hex characters at position $i: $hexByte',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
result.add(byte);
|
|
||||||
}
|
|
||||||
writeBytes(Uint8List.fromList(result));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Uint8List hex2Uint8List(String hex) {
|
||||||
|
// Validate hex string length is even and not empty
|
||||||
|
if (hex.isEmpty || hex.length % 2 != 0) {
|
||||||
|
throw FormatException('Invalid hex string length: ${hex.length}');
|
||||||
|
}
|
||||||
|
List<int> result = [];
|
||||||
|
for (int i = 0; i < hex.length ~/ 2; i++) {
|
||||||
|
final hexByte = hex.substring(i * 2, i * 2 + 2);
|
||||||
|
final byte = int.tryParse(hexByte, radix: 16);
|
||||||
|
if (byte == null) {
|
||||||
|
throw FormatException('Invalid hex characters at position $i: $hexByte');
|
||||||
|
}
|
||||||
|
result.add(byte);
|
||||||
|
}
|
||||||
|
return Uint8List.fromList(result);
|
||||||
|
}
|
||||||
|
|
||||||
// Command codes (to device)
|
// Command codes (to device)
|
||||||
const int cmdAppStart = 1;
|
const int cmdAppStart = 1;
|
||||||
const int cmdSendTxtMsg = 2;
|
const int cmdSendTxtMsg = 2;
|
||||||
@@ -151,11 +169,14 @@ const int cmdGetContactByKey = 30;
|
|||||||
const int cmdGetChannel = 31;
|
const int cmdGetChannel = 31;
|
||||||
const int cmdSetChannel = 32;
|
const int cmdSetChannel = 32;
|
||||||
const int cmdSendTracePath = 36;
|
const int cmdSendTracePath = 36;
|
||||||
const int cmdGetRadioSettings = 57;
|
const int cmdSetOtherParams = 38;
|
||||||
|
const int cmdSendAnonReq = 57;
|
||||||
const int cmdGetTelemetryReq = 39;
|
const int cmdGetTelemetryReq = 39;
|
||||||
const int cmdGetCustomVar = 40;
|
const int cmdGetCustomVar = 40;
|
||||||
const int cmdSetCustomVar = 41;
|
const int cmdSetCustomVar = 41;
|
||||||
const int cmdSendBinaryReq = 50;
|
const int cmdSendBinaryReq = 50;
|
||||||
|
const int cmdSetAutoAddConfig = 58;
|
||||||
|
const int cmdGetAutoAddConfig = 59;
|
||||||
|
|
||||||
// Text message types
|
// Text message types
|
||||||
const int txtTypePlain = 0;
|
const int txtTypePlain = 0;
|
||||||
@@ -166,7 +187,7 @@ const int reqTypeGetStatus = 0x01;
|
|||||||
const int reqTypeKeepAlive = 0x02;
|
const int reqTypeKeepAlive = 0x02;
|
||||||
const int reqTypeGetTelemetry = 0x03;
|
const int reqTypeGetTelemetry = 0x03;
|
||||||
const int reqTypeGetAccessList = 0x05;
|
const int reqTypeGetAccessList = 0x05;
|
||||||
const int reqTypeGetNeighbours = 0x06;
|
const int reqTypeGetNeighbors = 0x06;
|
||||||
|
|
||||||
// Repeater response codes
|
// Repeater response codes
|
||||||
const int respServerLoginOk = 0;
|
const int respServerLoginOk = 0;
|
||||||
@@ -189,8 +210,8 @@ const int respCodeDeviceInfo = 13;
|
|||||||
const int respCodeContactMsgRecvV3 = 16;
|
const int respCodeContactMsgRecvV3 = 16;
|
||||||
const int respCodeChannelMsgRecvV3 = 17;
|
const int respCodeChannelMsgRecvV3 = 17;
|
||||||
const int respCodeChannelInfo = 18;
|
const int respCodeChannelInfo = 18;
|
||||||
const int respCodeRadioSettings = 25;
|
|
||||||
const int respCodeCustomVars = 21;
|
const int respCodeCustomVars = 21;
|
||||||
|
const int respCodeAutoAddConfig = 25;
|
||||||
|
|
||||||
// Push codes (async from device)
|
// Push codes (async from device)
|
||||||
const int pushCodeAdvert = 0x80;
|
const int pushCodeAdvert = 0x80;
|
||||||
@@ -212,6 +233,42 @@ const int advTypeRepeater = 2;
|
|||||||
const int advTypeRoom = 3;
|
const int advTypeRoom = 3;
|
||||||
const int advTypeSensor = 4;
|
const int advTypeSensor = 4;
|
||||||
|
|
||||||
|
// Payload Types
|
||||||
|
const int payloadTypeREQ =
|
||||||
|
0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
|
||||||
|
const int payloadTypeRESPONSE =
|
||||||
|
0x01; // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
|
||||||
|
const int payloadTypeTXTMSG =
|
||||||
|
0x02; // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text)
|
||||||
|
const int payloadTypeACK = 0x03; // a simple ack
|
||||||
|
const int payloadTypeADVERT = 0x04; // a node advertising its Identity
|
||||||
|
const int payloadTypeGRPTXT =
|
||||||
|
0x05; // an (unverified) group text message (prefixed with channel hash, MAC) (enc data: timestamp, "name: msg")
|
||||||
|
const int payloadTypeGRPDATA =
|
||||||
|
0x06; // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: timestamp, blob)
|
||||||
|
const int payloadTypeANONREQ =
|
||||||
|
0x07; // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...)
|
||||||
|
const int payloadTypePATH =
|
||||||
|
0x08; // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra)
|
||||||
|
const int payloadTypeTRACE = 0x09; // trace a path, collecting SNI for each hop
|
||||||
|
const int payloadTypeMULTIPART = 0x0A; // packet is one of a set of packets
|
||||||
|
const int payloadTypeCONTROL = 0x0B; // a control/discovery packet
|
||||||
|
//...
|
||||||
|
const int payloadTypeRawCustom =
|
||||||
|
0x0F; // custom packet as raw bytes, for applications with custom encryption, payloads, etc
|
||||||
|
|
||||||
|
//auto-add flags
|
||||||
|
const int autoAddOverwriteOldestFlag =
|
||||||
|
1 << 0; // 0x01 - overwrite oldest non-favourite when full
|
||||||
|
const int autoAddChatFlag =
|
||||||
|
1 << 1; // 0x02 - auto-add Chat (Companion) (ADV_TYPE_CHAT)
|
||||||
|
const int autoAddRepeaterFlag =
|
||||||
|
1 << 2; // 0x04 - auto-add Repeater (ADV_TYPE_REPEATER)
|
||||||
|
const int autoAddRoomServerFlag =
|
||||||
|
1 << 3; // 0x08 - auto-add Room Server (ADV_TYPE_ROOM)
|
||||||
|
const int autoAddSensorFlag =
|
||||||
|
1 << 4; // 0x10 - auto-add Sensor (ADV_TYPE_SENSOR)
|
||||||
|
|
||||||
// Sizes
|
// Sizes
|
||||||
const int pubKeySize = 32;
|
const int pubKeySize = 32;
|
||||||
const int maxPathSize = 64;
|
const int maxPathSize = 64;
|
||||||
@@ -255,13 +312,14 @@ int _minPositive(int a, int b) {
|
|||||||
const int contactPubKeyOffset = 1;
|
const int contactPubKeyOffset = 1;
|
||||||
const int contactTypeOffset = 33;
|
const int contactTypeOffset = 33;
|
||||||
const int contactFlagsOffset = 34;
|
const int contactFlagsOffset = 34;
|
||||||
|
const int contactFlagFavorite = 0x01;
|
||||||
const int contactPathLenOffset = 35;
|
const int contactPathLenOffset = 35;
|
||||||
const int contactPathOffset = 36;
|
const int contactPathOffset = 36;
|
||||||
const int contactNameOffset = 100;
|
const int contactNameOffset = 100;
|
||||||
const int contactTimestampOffset = 132;
|
const int contactTimestampOffset = 132;
|
||||||
const int contactLatOffset = 136;
|
const int contactLatOffset = 136;
|
||||||
const int contactLonOffset = 140;
|
const int contactLonOffset = 140;
|
||||||
const int contactLastmodOffset = 144;
|
const int contactLastModOffset = 144;
|
||||||
const int contactFrameSize = 148;
|
const int contactFrameSize = 148;
|
||||||
|
|
||||||
// Message frame offsets
|
// Message frame offsets
|
||||||
@@ -639,16 +697,15 @@ Uint8List buildGetContactByKeyFrame(Uint8List pubKey) {
|
|||||||
return writer.toBytes();
|
return writer.toBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build CMD_GET_RADIO_SETTINGS frame
|
|
||||||
Uint8List buildGetRadioSettingsFrame() {
|
|
||||||
return Uint8List.fromList([cmdGetRadioSettings]);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Build CMD_GET_CUSTOM_VARS frame
|
//Build CMD_GET_CUSTOM_VARS frame
|
||||||
Uint8List buildGetCustomVarsFrame() {
|
Uint8List buildGetCustomVarsFrame() {
|
||||||
return Uint8List.fromList([cmdGetCustomVar]);
|
return Uint8List.fromList([cmdGetCustomVar]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Uint8List buildGetAutoAddFlagsFrame() {
|
||||||
|
return Uint8List.fromList([cmdGetAutoAddConfig]);
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate LoRa airtime for a packet
|
// Calculate LoRa airtime for a packet
|
||||||
// Based on Semtech SX127x datasheet formula
|
// Based on Semtech SX127x datasheet formula
|
||||||
// Returns airtime in milliseconds
|
// Returns airtime in milliseconds
|
||||||
@@ -773,10 +830,10 @@ Uint8List buildExportContactFrame(Uint8List pubKey) {
|
|||||||
|
|
||||||
// Build a import contact frame
|
// Build a import contact frame
|
||||||
// [cmd][contact_frame x98+]
|
// [cmd][contact_frame x98+]
|
||||||
Uint8List buildImportContactFrame(String contactFrame) {
|
Uint8List buildImportContactFrame(Uint8List contactFrame) {
|
||||||
final writer = BufferWriter();
|
final writer = BufferWriter();
|
||||||
writer.writeByte(cmdImportContact);
|
writer.writeByte(cmdImportContact);
|
||||||
writer.writeHex(contactFrame);
|
writer.writeBytes(contactFrame);
|
||||||
return writer.toBytes();
|
return writer.toBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -788,3 +845,42 @@ Uint8List buildZeroHopContact(Uint8List pubKey) {
|
|||||||
writer.writeBytes(pubKey);
|
writer.writeBytes(pubKey);
|
||||||
return writer.toBytes();
|
return writer.toBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build CMD_SET_OTHER_PARAMS frame
|
||||||
|
// Format: [cmd][allowTelemetryFlags][advertLocationPolicy][multiAcks]
|
||||||
|
Uint8List buildSetOtherParamsFrame(
|
||||||
|
int allowTelemetryFlags,
|
||||||
|
int advertLocationPolicy,
|
||||||
|
int multiAcks,
|
||||||
|
) {
|
||||||
|
final writer = BufferWriter();
|
||||||
|
writer.writeByte(cmdSetOtherParams);
|
||||||
|
//Going forward the app will just set Auto Add Contacts to disabled, and use the filter flags
|
||||||
|
//Allow Auto Add Contacts use inverted logic (0x01 = disabled, 0x00 = enabled).
|
||||||
|
writer.writeByte(0x01);
|
||||||
|
writer.writeByte(allowTelemetryFlags); // Allow Telemetry Flags
|
||||||
|
writer.writeByte(advertLocationPolicy); // Advertisement Location Policy
|
||||||
|
writer.writeByte(multiAcks); // Multi Acknowledgements
|
||||||
|
return writer.toBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build CMD_SET_AUTO_ADD_CONFIG frame
|
||||||
|
// Format: [cmd][flags]
|
||||||
|
Uint8List buildSetAutoAddConfigFrame({
|
||||||
|
required bool autoAddChat,
|
||||||
|
required bool autoAddRepeater,
|
||||||
|
required bool autoAddRoomServer,
|
||||||
|
required bool autoAddSensor,
|
||||||
|
required bool overwriteOldest,
|
||||||
|
}) {
|
||||||
|
final writer = BufferWriter();
|
||||||
|
writer.writeByte(cmdSetAutoAddConfig);
|
||||||
|
int flags = 0;
|
||||||
|
if (autoAddChat) flags |= autoAddChatFlag;
|
||||||
|
if (autoAddRepeater) flags |= autoAddRepeaterFlag;
|
||||||
|
if (autoAddRoomServer) flags |= autoAddRoomServerFlag;
|
||||||
|
if (autoAddSensor) flags |= autoAddSensorFlag;
|
||||||
|
if (overwriteOldest) flags |= autoAddOverwriteOldestFlag;
|
||||||
|
writer.writeByte(flags);
|
||||||
|
return writer.toBytes();
|
||||||
|
}
|
||||||
|
|||||||
+175
-161
@@ -1,4 +1,6 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
import 'package:meshcore_open/utils/app_logger.dart';
|
||||||
|
|
||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
|
|
||||||
class CayenneLpp {
|
class CayenneLpp {
|
||||||
@@ -84,180 +86,192 @@ class CayenneLpp {
|
|||||||
static List<Map<String, dynamic>> parse(Uint8List bytes) {
|
static List<Map<String, dynamic>> parse(Uint8List bytes) {
|
||||||
final buffer = BufferReader(bytes);
|
final buffer = BufferReader(bytes);
|
||||||
final telemetry = <Map<String, dynamic>>[];
|
final telemetry = <Map<String, dynamic>>[];
|
||||||
|
try {
|
||||||
|
while (buffer.remaining >= 2) {
|
||||||
|
final channel = buffer.readUInt8();
|
||||||
|
final type = buffer.readUInt8();
|
||||||
|
|
||||||
while (buffer.remaining >= 2) {
|
if (channel == 0 && type == 0) {
|
||||||
final channel = buffer.readUInt8();
|
break;
|
||||||
final type = buffer.readUInt8();
|
}
|
||||||
|
|
||||||
if (channel == 0 && type == 0) {
|
switch (type) {
|
||||||
break;
|
case lppGenericSensor:
|
||||||
}
|
telemetry.add({
|
||||||
|
'channel': channel,
|
||||||
switch (type) {
|
'type': type,
|
||||||
case lppGenericSensor:
|
'value': buffer.readUInt32BE(),
|
||||||
telemetry.add({
|
});
|
||||||
'channel': channel,
|
break;
|
||||||
'type': type,
|
case lppLuminosity:
|
||||||
'value': buffer.readUInt32BE(),
|
telemetry.add({
|
||||||
});
|
'channel': channel,
|
||||||
break;
|
'type': type,
|
||||||
case lppLuminosity:
|
'value': buffer.readUInt16BE(),
|
||||||
telemetry.add({
|
});
|
||||||
'channel': channel,
|
break;
|
||||||
'type': type,
|
case lppPresence:
|
||||||
'value': buffer.readUInt16BE(),
|
telemetry.add({
|
||||||
});
|
'channel': channel,
|
||||||
break;
|
'type': type,
|
||||||
case lppPresence:
|
'value': buffer.readUInt8(),
|
||||||
telemetry.add({
|
});
|
||||||
'channel': channel,
|
break;
|
||||||
'type': type,
|
case lppTemperature:
|
||||||
'value': buffer.readUInt8(),
|
telemetry.add({
|
||||||
});
|
'channel': channel,
|
||||||
break;
|
'type': type,
|
||||||
case lppTemperature:
|
'value': buffer.readInt16BE() / 10,
|
||||||
telemetry.add({
|
});
|
||||||
'channel': channel,
|
break;
|
||||||
'type': type,
|
case lppRelativeHumidity:
|
||||||
'value': buffer.readInt16BE() / 10,
|
telemetry.add({
|
||||||
});
|
'channel': channel,
|
||||||
break;
|
'type': type,
|
||||||
case lppRelativeHumidity:
|
'value': buffer.readUInt8() / 2,
|
||||||
telemetry.add({
|
});
|
||||||
'channel': channel,
|
break;
|
||||||
'type': type,
|
case lppBarometricPressure:
|
||||||
'value': buffer.readUInt8() / 2,
|
telemetry.add({
|
||||||
});
|
'channel': channel,
|
||||||
break;
|
'type': type,
|
||||||
case lppBarometricPressure:
|
'value': buffer.readUInt16BE() / 10,
|
||||||
telemetry.add({
|
});
|
||||||
'channel': channel,
|
break;
|
||||||
'type': type,
|
case lppVoltage:
|
||||||
'value': buffer.readUInt16BE() / 10,
|
telemetry.add({
|
||||||
});
|
'channel': channel,
|
||||||
break;
|
'type': type,
|
||||||
case lppVoltage:
|
'value': buffer.readInt16BE() / 100,
|
||||||
telemetry.add({
|
});
|
||||||
'channel': channel,
|
break;
|
||||||
'type': type,
|
case lppCurrent:
|
||||||
'value': buffer.readInt16BE() / 100,
|
telemetry.add({
|
||||||
});
|
'channel': channel,
|
||||||
break;
|
'type': type,
|
||||||
case lppCurrent:
|
'value': buffer.readInt16BE() / 1000,
|
||||||
telemetry.add({
|
});
|
||||||
'channel': channel,
|
break;
|
||||||
'type': type,
|
case lppPercentage:
|
||||||
'value': buffer.readInt16BE() / 1000,
|
telemetry.add({
|
||||||
});
|
'channel': channel,
|
||||||
break;
|
'type': type,
|
||||||
case lppPercentage:
|
'value': buffer.readUInt8(),
|
||||||
telemetry.add({
|
});
|
||||||
'channel': channel,
|
break;
|
||||||
'type': type,
|
case lppConcentration:
|
||||||
'value': buffer.readUInt8(),
|
telemetry.add({
|
||||||
});
|
'channel': channel,
|
||||||
break;
|
'type': type,
|
||||||
case lppConcentration:
|
'value': buffer.readUInt16BE(),
|
||||||
telemetry.add({
|
});
|
||||||
'channel': channel,
|
break;
|
||||||
'type': type,
|
case lppPower:
|
||||||
'value': buffer.readUInt16BE(),
|
telemetry.add({
|
||||||
});
|
'channel': channel,
|
||||||
break;
|
'type': type,
|
||||||
case lppPower:
|
'value': buffer.readUInt16BE(),
|
||||||
telemetry.add({
|
});
|
||||||
'channel': channel,
|
break;
|
||||||
'type': type,
|
case lppGps:
|
||||||
'value': buffer.readUInt16BE(),
|
telemetry.add({
|
||||||
});
|
'channel': channel,
|
||||||
break;
|
'type': type,
|
||||||
case lppGps:
|
'value': {
|
||||||
telemetry.add({
|
'latitude': buffer.readInt24BE() / 10000,
|
||||||
'channel': channel,
|
'longitude': buffer.readInt24BE() / 10000,
|
||||||
'type': type,
|
'altitude': buffer.readInt24BE() / 100,
|
||||||
'value': {
|
},
|
||||||
'latitude': buffer.readInt24BE() / 10000,
|
});
|
||||||
'longitude': buffer.readInt24BE() / 10000,
|
break;
|
||||||
'altitude': buffer.readInt24BE() / 100,
|
default:
|
||||||
},
|
return telemetry;
|
||||||
});
|
}
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return telemetry;
|
|
||||||
}
|
}
|
||||||
|
return telemetry;
|
||||||
|
} catch (e) {
|
||||||
|
// Handle parsing errors, possibly due to malformed data
|
||||||
|
appLogger.error('Error parsing Cayenne LPP data: $e');
|
||||||
|
// Return any telemetry parsed so far to preserve partial data
|
||||||
|
return telemetry;
|
||||||
}
|
}
|
||||||
|
|
||||||
return telemetry;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<Map<String, dynamic>> parseByChannel(Uint8List bytes) {
|
static List<Map<String, dynamic>> parseByChannel(Uint8List bytes) {
|
||||||
final buffer = BufferReader(bytes);
|
final buffer = BufferReader(bytes);
|
||||||
final Map<int, Map<String, dynamic>> channels = {};
|
final Map<int, Map<String, dynamic>> channels = {};
|
||||||
|
try {
|
||||||
|
while (buffer.remaining >= 2) {
|
||||||
|
final channel = buffer.readUInt8();
|
||||||
|
final type = buffer.readUInt8();
|
||||||
|
|
||||||
while (buffer.remaining >= 2) {
|
// Optional: stop on padding (00 00)
|
||||||
final channel = buffer.readUInt8();
|
if (channel == 0 && type == 0) {
|
||||||
final type = buffer.readUInt8();
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Optional: stop on padding (00 00)
|
final channelData = channels.putIfAbsent(
|
||||||
if (channel == 0 && type == 0) {
|
channel,
|
||||||
break;
|
() => {'channel': channel, 'values': <String, dynamic>{}},
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case lppGenericSensor:
|
||||||
|
channelData['values']['generic'] = buffer.readUInt32BE();
|
||||||
|
break;
|
||||||
|
case lppLuminosity:
|
||||||
|
channelData['values']['luminosity'] = buffer.readUInt16BE();
|
||||||
|
break;
|
||||||
|
case lppPresence:
|
||||||
|
channelData['values']['presence'] = buffer.readUInt8() != 0;
|
||||||
|
break;
|
||||||
|
case lppTemperature:
|
||||||
|
channelData['values']['temperature'] = buffer.readInt16BE() / 10.0;
|
||||||
|
break;
|
||||||
|
case lppRelativeHumidity:
|
||||||
|
channelData['values']['humidity'] = buffer.readUInt8() / 2.0;
|
||||||
|
break;
|
||||||
|
case lppBarometricPressure:
|
||||||
|
channelData['values']['pressure'] = buffer.readUInt16BE() / 10.0;
|
||||||
|
break;
|
||||||
|
case lppVoltage:
|
||||||
|
channelData['values']['voltage'] = buffer.readInt16BE() / 100.0;
|
||||||
|
break;
|
||||||
|
case lppCurrent:
|
||||||
|
channelData['values']['current'] = buffer.readInt16BE() / 1000.0;
|
||||||
|
break;
|
||||||
|
case lppPercentage:
|
||||||
|
channelData['values']['percentage'] = buffer.readUInt8();
|
||||||
|
break;
|
||||||
|
case lppConcentration:
|
||||||
|
channelData['values']['concentration'] = buffer.readUInt16BE();
|
||||||
|
break;
|
||||||
|
case lppPower:
|
||||||
|
channelData['values']['power'] = buffer.readUInt16BE();
|
||||||
|
break;
|
||||||
|
case lppGps:
|
||||||
|
channelData['values']['gps'] = {
|
||||||
|
'latitude': buffer.readInt24BE() / 10000.0,
|
||||||
|
'longitude': buffer.readInt24BE() / 10000.0,
|
||||||
|
'altitude': buffer.readInt24BE() / 100.0,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
// Add more types as needed...
|
||||||
|
default:
|
||||||
|
//Stopped parsing to avoid misalignment
|
||||||
|
return channels.values.toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final channelData = channels.putIfAbsent(
|
final List<Map<String, dynamic>> channelsOut = channels.values.toList();
|
||||||
channel,
|
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
|
||||||
() => {'channel': channel, 'values': <String, dynamic>{}},
|
return channelsOut;
|
||||||
);
|
} catch (e) {
|
||||||
|
// Handle parsing errors, possibly due to malformed data
|
||||||
switch (type) {
|
appLogger.error('Error parsing Cayenne LPP data: $e');
|
||||||
case lppGenericSensor:
|
return <
|
||||||
channelData['values']['generic'] = buffer.readUInt32BE();
|
Map<String, dynamic>
|
||||||
break;
|
>[]; // Return an empty list on error to avoid crashing the app
|
||||||
case lppLuminosity:
|
|
||||||
channelData['values']['luminosity'] = buffer.readUInt16BE();
|
|
||||||
break;
|
|
||||||
case lppPresence:
|
|
||||||
channelData['values']['presence'] = buffer.readUInt8() != 0;
|
|
||||||
break;
|
|
||||||
case lppTemperature:
|
|
||||||
channelData['values']['temperature'] = buffer.readInt16BE() / 10.0;
|
|
||||||
break;
|
|
||||||
case lppRelativeHumidity:
|
|
||||||
channelData['values']['humidity'] = buffer.readUInt8() / 2.0;
|
|
||||||
break;
|
|
||||||
case lppBarometricPressure:
|
|
||||||
channelData['values']['pressure'] = buffer.readUInt16BE() / 10.0;
|
|
||||||
break;
|
|
||||||
case lppVoltage:
|
|
||||||
channelData['values']['voltage'] = buffer.readInt16BE() / 100.0;
|
|
||||||
break;
|
|
||||||
case lppCurrent:
|
|
||||||
channelData['values']['current'] = buffer.readInt16BE() / 1000.0;
|
|
||||||
break;
|
|
||||||
case lppPercentage:
|
|
||||||
channelData['values']['percentage'] = buffer.readUInt8();
|
|
||||||
break;
|
|
||||||
case lppConcentration:
|
|
||||||
channelData['values']['concentration'] = buffer.readUInt16BE();
|
|
||||||
break;
|
|
||||||
case lppPower:
|
|
||||||
channelData['values']['power'] = buffer.readUInt16BE();
|
|
||||||
break;
|
|
||||||
case lppGps:
|
|
||||||
channelData['values']['gps'] = {
|
|
||||||
'latitude': buffer.readInt24BE() / 10000.0,
|
|
||||||
'longitude': buffer.readInt24BE() / 10000.0,
|
|
||||||
'altitude': buffer.readInt24BE() / 100.0,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
// Add more types as needed...
|
|
||||||
default:
|
|
||||||
// Unknown type: skip or handle error?
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<Map<String, dynamic>> channelsOut = channels.values.toList();
|
|
||||||
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
|
|
||||||
return channelsOut;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
|
|
||||||
|
class LosIcon extends StatelessWidget {
|
||||||
|
final double size;
|
||||||
|
final Color? color;
|
||||||
|
|
||||||
|
const LosIcon({super.key, this.size = 24, this.color});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final iconTheme = IconTheme.of(context);
|
||||||
|
final iconColor =
|
||||||
|
color ??
|
||||||
|
iconTheme.color ??
|
||||||
|
theme.iconTheme.color ??
|
||||||
|
theme.colorScheme.onSurface;
|
||||||
|
|
||||||
|
return Icon(Symbols.elevation, size: size, color: iconColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
+267
-4
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"channels_channelDeleteFailed": "Неуспешно изтриване на канала \"{name}\"",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@@locale": "bg",
|
"@@locale": "bg",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Контакти",
|
"nav_contacts": "Контакти",
|
||||||
@@ -334,6 +342,8 @@
|
|||||||
"channels_publicChannel": "Публичен канал",
|
"channels_publicChannel": "Публичен канал",
|
||||||
"channels_privateChannel": "Частен канал",
|
"channels_privateChannel": "Частен канал",
|
||||||
"channels_editChannel": "Редактирай канал",
|
"channels_editChannel": "Редактирай канал",
|
||||||
|
"channels_muteChannel": "Заглуши канала",
|
||||||
|
"channels_unmuteChannel": "Включи известията на канала",
|
||||||
"channels_deleteChannel": "Изтрий канала",
|
"channels_deleteChannel": "Изтрий канала",
|
||||||
"channels_deleteChannelConfirm": "Изтрий \"{name}\"? Това не може да бъде отменено.",
|
"channels_deleteChannelConfirm": "Изтрий \"{name}\"? Това не може да бъде отменено.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
@@ -1351,12 +1361,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighboursSubtitle": "Преглед на съседни възли с нулев скок.",
|
"repeater_neighborsSubtitle": "Преглед на съседни възли с нулев скок.",
|
||||||
"repeater_neighbours": "Съседи",
|
"repeater_neighbors": "Съседи",
|
||||||
"neighbors_receivedData": "Получени данни за съседи",
|
"neighbors_receivedData": "Получени данни за съседи",
|
||||||
"neighbors_requestTimedOut": "Съседите поискат изтичане на време.",
|
"neighbors_requestTimedOut": "Съседите поискат изтичане на време.",
|
||||||
"neighbors_errorLoading": "Грешка при зареждане на съседи: {error}",
|
"neighbors_errorLoading": "Грешка при зареждане на съседи: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Повторители Съседи",
|
"neighbors_repeatersNeighbors": "Повторители Съседи",
|
||||||
"neighbors_noData": "Няма налични данни за съседи.",
|
"neighbors_noData": "Няма налични данни за съседи.",
|
||||||
"channels_createPrivateChannel": "Създай Частен Канал",
|
"channels_createPrivateChannel": "Създай Частен Канал",
|
||||||
"channels_joinPrivateChannel": "Присъедини се към Частен Канал",
|
"channels_joinPrivateChannel": "Присъедини се към Частен Канал",
|
||||||
@@ -1552,6 +1562,8 @@
|
|||||||
"contacts_clipboardEmpty": "Клипборда е празна.",
|
"contacts_clipboardEmpty": "Клипборда е празна.",
|
||||||
"contacts_invalidAdvertFormat": "Невалидни данни за контакт",
|
"contacts_invalidAdvertFormat": "Невалидни данни за контакт",
|
||||||
"appSettings_languageRu": "Руски",
|
"appSettings_languageRu": "Руски",
|
||||||
|
"appSettings_enableMessageTracing": "Разрешаване на проследяване на съобщения",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Показване на подробни метаданни за маршрутизация и синхронизация за съобщения",
|
||||||
"contacts_contactImported": "Контактът е импортиран.",
|
"contacts_contactImported": "Контактът е импортиран.",
|
||||||
"contacts_zeroHopAdvert": "Реклама без скок",
|
"contacts_zeroHopAdvert": "Реклама без скок",
|
||||||
"contacts_contactImportFailed": "Контактът не е успешно импортиран.",
|
"contacts_contactImportFailed": "Контактът не е успешно импортиран.",
|
||||||
@@ -1594,7 +1606,258 @@
|
|||||||
"scanner_bluetoothOff": "Bluetooth е изключен.",
|
"scanner_bluetoothOff": "Bluetooth е изключен.",
|
||||||
"scanner_enableBluetooth": "Активирайте Bluetooth",
|
"scanner_enableBluetooth": "Активирайте Bluetooth",
|
||||||
"scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.",
|
"scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.",
|
||||||
|
"scanner_chromeRequired": "Изисква се браузър Chrome",
|
||||||
|
"scanner_chromeRequiredMessage": "Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.",
|
||||||
|
"snrIndicator_lastSeen": "Последно видян",
|
||||||
|
"snrIndicator_nearByRepeaters": "Близки повтарящи се устройства",
|
||||||
|
"chat_ShowAllPaths": "Покажи всички пътища",
|
||||||
"settings_clientRepeatSubtitle": "Позволете на това устройство да предава пакети към мрежата за други устройства.",
|
"settings_clientRepeatSubtitle": "Позволете на това устройство да предава пакети към мрежата за други устройства.",
|
||||||
"settings_clientRepeatFreqWarning": "За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.",
|
"settings_clientRepeatFreqWarning": "За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.",
|
||||||
"settings_clientRepeat": "Без електричество – повторение"
|
"settings_clientRepeat": "Без електричество – повторение",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)",
|
||||||
|
"appSettings_unitsTitle": "единици",
|
||||||
|
"appSettings_unitsMetric": "Метрика (m / km)",
|
||||||
|
"appSettings_unitsImperial": "Имперска (ft / mi)",
|
||||||
|
"map_lineOfSight": "Линия на видимост",
|
||||||
|
"map_losScreenTitle": "Линия на видимост",
|
||||||
|
"losSelectStartEnd": "Изберете начални и крайни възли за LOS.",
|
||||||
|
"losRunFailed": "Проверката на пряката видимост е неуспешна: {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Изчистете всички точки",
|
||||||
|
"losRunToViewElevationProfile": "Стартирайте LOS, за да видите профила на надморската височина",
|
||||||
|
"losMenuTitle": "LOS меню",
|
||||||
|
"losMenuSubtitle": "Докоснете възли или натиснете продължително карта за персонализирани точки",
|
||||||
|
"losShowDisplayNodes": "Показване на възли на дисплея",
|
||||||
|
"losCustomPoints": "Персонализирани точки",
|
||||||
|
"losCustomPointLabel": "Персонализирано {index}",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Точка А",
|
||||||
|
"losPointB": "Точка Б",
|
||||||
|
"losAntennaA": "Антена A: {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Антена B: {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Стартирайте LOS",
|
||||||
|
"losNoElevationData": "Няма данни за надморска височина",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, чист LOS, минимално разстояние {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, блокиран от {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "LOS: проверка...",
|
||||||
|
"losStatusNoData": "LOS: няма данни",
|
||||||
|
"losStatusSummary": "LOS: {clear}/{total} ясно, {blocked} блокирано, {unknown} неизвестно",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Няма налични данни за надморска височина за една или повече проби.",
|
||||||
|
"losErrorInvalidInput": "Невалидни данни за точки/надморска височина за изчисляване на LOS.",
|
||||||
|
"losRenameCustomPoint": "Преименувайте персонализирана точка",
|
||||||
|
"losPointName": "Име на точката",
|
||||||
|
"losShowPanelTooltip": "Показване на LOS панел",
|
||||||
|
"losHidePanelTooltip": "Скриване на LOS панела",
|
||||||
|
"losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Радиохоризонт",
|
||||||
|
"losLegendLosBeam": "Линия на видимост",
|
||||||
|
"losLegendTerrain": "Терен",
|
||||||
|
"losFrequencyLabel": "Честота",
|
||||||
|
"losFrequencyInfoTooltip": "Преглед на детайли за изчислението",
|
||||||
|
"losFrequencyDialogTitle": "Изчисляване на радиохоризонта",
|
||||||
|
"losFrequencyDialogDescription": "Започвайки от k={baselineK} при {baselineFreq} MHz, изчислението коригира k-фактора за текущата {frequencyMHz} MHz лента, която определя границата на извития радиохоризонт.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listFilter_removeFromFavorites": "Премахване от списъка с любими",
|
||||||
|
"listFilter_addToFavorites": "Добави към любими",
|
||||||
|
"listFilter_favorites": "Любими",
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_searchFavorites": "Търсене на {number}{str} любими...",
|
||||||
|
"contacts_searchRoomServers": "Търсене на {number}{str} сървъри в стаята...",
|
||||||
|
"contacts_unread": "Непрочетено",
|
||||||
|
"contacts_searchRepeaters": "Търсене на {number}{str} повтарящи се...",
|
||||||
|
"contacts_searchContactsNoNumber": "Търси контакти...",
|
||||||
|
"contacts_searchUsers": "Търсене на {number}{str} потребители...",
|
||||||
|
"contactsSettings_title": "Настройки на контактите",
|
||||||
|
"contactsSettings_autoAddTitle": "Автоматично откриване",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Автоматично добавяне на потребители",
|
||||||
|
"contactsSettings_otherTitle": "Други настройки свързани с контакти",
|
||||||
|
"settings_contactSettingsSubtitle": "Настройки за добавяне на контакти.",
|
||||||
|
"settings_contactSettings": "Настройки за контакти",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Автоматично добавяне на датчици",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Автоматично добавяне на сървъри на стаите",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Позволи на спътника да добавя автоматично откритите сървъри на стаите.",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Автоматично добавяне на повтарящи се елементи",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Позволи на спътника да добавя автоматично откритите потребители.",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Позволи на спътника да добавя автоматично откритите повтарящи се устройства.",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Позволи на спътника да добавя автоматично откритите датчици.",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Премахни най-старото",
|
||||||
|
"discoveredContacts_Title": "Открити контакти",
|
||||||
|
"discoveredContacts_searchHint": "Търсене на открити контакти",
|
||||||
|
"discoveredContacts_noMatching": "Няма съвпадащи контакти",
|
||||||
|
"discoveredContacts_contactAdded": "Контакт добавен",
|
||||||
|
"discoveredContacts_copyContact": "Копирай контакт в клипборда",
|
||||||
|
"discoveredContacts_deleteContact": "Изтрий контакт",
|
||||||
|
"discoveredContacts_addContact": "Добави контакт",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Когато списъкът с контакти е пълен, най-старият неключов контакт ще бъде заменен.",
|
||||||
|
"discoveredContacts_deleteContactAll": "Изтриване на Всички Открити Контакти",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Сигурни ли сте, че искате да изтриете всички открити контакти?",
|
||||||
|
"common_deleteAll": "Изтрий всичко",
|
||||||
|
"map_guessedLocation": "Предполагано местоположение",
|
||||||
|
"map_showGuessedLocations": "Покажете местоположенията на предположените възли.",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenTitle": "Свържете се чрез USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenSubtitle": "Изберете открития сериен уред и свържете директно към вашия MeshCore възел.",
|
||||||
|
"usbScreenStatus": "Изберете USB устройство",
|
||||||
|
"usbScreenNote": "USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.",
|
||||||
|
"usbScreenEmptyState": "Няма открити USB устройства. Включете едно и опитайте отново.",
|
||||||
|
"usbErrorPermissionDenied": "Не беше разрешено достъпът през USB.",
|
||||||
|
"usbErrorDeviceMissing": "Избраното USB устройство вече не е налично.",
|
||||||
|
"usbErrorInvalidPort": "Изберете валитно USB устройство.",
|
||||||
|
"usbErrorBusy": "Друг мол за свързване през USB вече е в процес на изпълнение.",
|
||||||
|
"usbErrorNotConnected": "Няма свързано USB устройство.",
|
||||||
|
"usbErrorOpenFailed": "Не успях да отворя избраното USB устройство.",
|
||||||
|
"usbErrorConnectFailed": "Не успях да се свържа с избраното USB устройство.",
|
||||||
|
"usbErrorUnsupported": "USB серийната комуникация не се поддържа на тази платформа.",
|
||||||
|
"usbErrorAlreadyActive": "USB връзката вече е активирана.",
|
||||||
|
"usbErrorNoDeviceSelected": "Няма избран USB устройство.",
|
||||||
|
"usbErrorPortClosed": "USB връзката не е активна.",
|
||||||
|
"usbFallbackDeviceName": "Устройство за четене на уеб серийни данни",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_connecting": "Свързване към USB устройство...",
|
||||||
|
"usbConnectionFailed": "Неуспешно свързване през USB: {error}",
|
||||||
|
"usbStatus_notConnected": "Изберете USB устройство",
|
||||||
|
"usbStatus_searching": "Търсене на USB устройства...",
|
||||||
|
"usbErrorConnectTimedOut": "Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка."
|
||||||
}
|
}
|
||||||
|
|||||||
+273
-10
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"channels_channelDeleteFailed": "Kanal {name} konnte nicht gelöscht werden",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@@locale": "de",
|
"@@locale": "de",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Kontakte",
|
"nav_contacts": "Kontakte",
|
||||||
@@ -288,8 +296,8 @@
|
|||||||
"contacts_filterContacts": "Filtert Kontakte...",
|
"contacts_filterContacts": "Filtert Kontakte...",
|
||||||
"contacts_noContactsMatchFilter": "Keine Kontakte passen zu Ihrem Filter",
|
"contacts_noContactsMatchFilter": "Keine Kontakte passen zu Ihrem Filter",
|
||||||
"contacts_noMembers": "Keine Mitglieder",
|
"contacts_noMembers": "Keine Mitglieder",
|
||||||
"contacts_lastSeenNow": "gerade gesehen",
|
"contacts_lastSeenNow": "kürzlich",
|
||||||
"contacts_lastSeenMinsAgo": "Letzte Sichtung vor {minutes} Minuten.",
|
"contacts_lastSeenMinsAgo": "~ {minutes} Min.",
|
||||||
"@contacts_lastSeenMinsAgo": {
|
"@contacts_lastSeenMinsAgo": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"minutes": {
|
"minutes": {
|
||||||
@@ -297,8 +305,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contacts_lastSeenHourAgo": "Letzte Sichtung vor 1 Stunde.",
|
"contacts_lastSeenHourAgo": "~ 1 Std.",
|
||||||
"contacts_lastSeenHoursAgo": "Letzte Sichtung vor {hours} Stunden.",
|
"contacts_lastSeenHoursAgo": "~ {hours} Std.",
|
||||||
"@contacts_lastSeenHoursAgo": {
|
"@contacts_lastSeenHoursAgo": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"hours": {
|
"hours": {
|
||||||
@@ -306,8 +314,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contacts_lastSeenDayAgo": "Letzte Sichtung vor 1 Tag",
|
"contacts_lastSeenDayAgo": "~ 1 Tag",
|
||||||
"contacts_lastSeenDaysAgo": "Letzte Sichtung {days} Tage zuvor",
|
"contacts_lastSeenDaysAgo": "~ {days} Tage",
|
||||||
"@contacts_lastSeenDaysAgo": {
|
"@contacts_lastSeenDaysAgo": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"days": {
|
"days": {
|
||||||
@@ -334,6 +342,8 @@
|
|||||||
"channels_publicChannel": "Öffentlicher Kanal",
|
"channels_publicChannel": "Öffentlicher Kanal",
|
||||||
"channels_privateChannel": "Privater Kanal",
|
"channels_privateChannel": "Privater Kanal",
|
||||||
"channels_editChannel": "Kanal bearbeiten",
|
"channels_editChannel": "Kanal bearbeiten",
|
||||||
|
"channels_muteChannel": "Kanal stummschalten",
|
||||||
|
"channels_unmuteChannel": "Kanal Stummschaltung aufheben",
|
||||||
"channels_deleteChannel": "Lösche den Kanal",
|
"channels_deleteChannel": "Lösche den Kanal",
|
||||||
"channels_deleteChannelConfirm": "Löschen von \"{name}\"? Dies kann nicht rückgängig gemacht werden.",
|
"channels_deleteChannelConfirm": "Löschen von \"{name}\"? Dies kann nicht rückgängig gemacht werden.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
@@ -1339,6 +1349,9 @@
|
|||||||
"listFilter_az": "A-Z",
|
"listFilter_az": "A-Z",
|
||||||
"listFilter_filters": "Filtere",
|
"listFilter_filters": "Filtere",
|
||||||
"listFilter_all": "Alle",
|
"listFilter_all": "Alle",
|
||||||
|
"listFilter_favorites": "Favoriten",
|
||||||
|
"listFilter_addToFavorites": "Zu Favoriten hinzufügen",
|
||||||
|
"listFilter_removeFromFavorites": "Aus Favoriten entfernen",
|
||||||
"listFilter_users": "Benutzer",
|
"listFilter_users": "Benutzer",
|
||||||
"listFilter_repeaters": "Repeater",
|
"listFilter_repeaters": "Repeater",
|
||||||
"listFilter_roomServers": "Raumserver",
|
"listFilter_roomServers": "Raumserver",
|
||||||
@@ -1351,12 +1364,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Nachbarn",
|
"repeater_neighbors": "Nachbarn",
|
||||||
"repeater_neighboursSubtitle": "Anzahl der Hop-Nachbarn anzeigen.",
|
"repeater_neighborsSubtitle": "Anzahl der Hop-Nachbarn anzeigen.",
|
||||||
"neighbors_receivedData": "Empfangene Nachbarsdaten",
|
"neighbors_receivedData": "Empfangene Nachbarsdaten",
|
||||||
"neighbors_requestTimedOut": "Anfrage durch Timeout fehlgeschlagen.",
|
"neighbors_requestTimedOut": "Anfrage durch Timeout fehlgeschlagen.",
|
||||||
"neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}",
|
"neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Nachbarn",
|
"neighbors_repeatersNeighbors": "Nachbarn",
|
||||||
"neighbors_noData": "Keine Nachbarsdaten verfügbar.",
|
"neighbors_noData": "Keine Nachbarsdaten verfügbar.",
|
||||||
"channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei",
|
"channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei",
|
||||||
"channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.",
|
"channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.",
|
||||||
@@ -1552,6 +1565,8 @@
|
|||||||
"contacts_invalidAdvertFormat": "Ungültige Kontaktdaten",
|
"contacts_invalidAdvertFormat": "Ungültige Kontaktdaten",
|
||||||
"contacts_clipboardEmpty": "Die Zwischenablage ist leer.",
|
"contacts_clipboardEmpty": "Die Zwischenablage ist leer.",
|
||||||
"appSettings_languageUk": "Ukrainisch",
|
"appSettings_languageUk": "Ukrainisch",
|
||||||
|
"appSettings_enableMessageTracing": "Nachrichtenverfolgung aktivieren",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen",
|
||||||
"contacts_contactImported": "Kontakt wurde importiert.",
|
"contacts_contactImported": "Kontakt wurde importiert.",
|
||||||
"contacts_contactImportFailed": "Kontakt konnte nicht importiert werden",
|
"contacts_contactImportFailed": "Kontakt konnte nicht importiert werden",
|
||||||
"contacts_zeroHopAdvert": "Zero-Hop-Ankündigung",
|
"contacts_zeroHopAdvert": "Zero-Hop-Ankündigung",
|
||||||
@@ -1620,9 +1635,257 @@
|
|||||||
"pathTrace_clearTooltip": "Pfad löschen",
|
"pathTrace_clearTooltip": "Pfad löschen",
|
||||||
"map_pathTraceCancelled": "Pfadverfolgung abgebrochen.",
|
"map_pathTraceCancelled": "Pfadverfolgung abgebrochen.",
|
||||||
"scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.",
|
"scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.",
|
||||||
|
"scanner_chromeRequired": "Chrome Browser erforderlich",
|
||||||
|
"scanner_chromeRequiredMessage": "Diese Webanwendung erfordert Google Chrome oder einen Chromium-basierten Browser für die Bluetooth-Unterstützung.",
|
||||||
"scanner_bluetoothOff": "Bluetooth ist deaktiviert.",
|
"scanner_bluetoothOff": "Bluetooth ist deaktiviert.",
|
||||||
"scanner_enableBluetooth": "Bluetooth aktivieren",
|
"scanner_enableBluetooth": "Bluetooth aktivieren",
|
||||||
|
"snrIndicator_lastSeen": "Zuletzt gesehen",
|
||||||
|
"snrIndicator_nearByRepeaters": "In der Nähe befindliche Repeater",
|
||||||
|
"chat_ShowAllPaths": "Alle Pfade anzeigen",
|
||||||
"settings_clientRepeat": "Wiederholung, ohne Stromanschluss",
|
"settings_clientRepeat": "Wiederholung, ohne Stromanschluss",
|
||||||
"settings_clientRepeatFreqWarning": "Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.",
|
"settings_clientRepeatFreqWarning": "Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.",
|
||||||
"settings_clientRepeatSubtitle": "Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen."
|
"settings_clientRepeatSubtitle": "Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen.",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "LOS-Höhendaten: Open-Meteo (CC BY 4.0)",
|
||||||
|
"appSettings_unitsTitle": "Einheiten",
|
||||||
|
"appSettings_unitsMetric": "Metrisch (m/km)",
|
||||||
|
"appSettings_unitsImperial": "Imperial (ft/mi)",
|
||||||
|
"map_lineOfSight": "Sichtlinie",
|
||||||
|
"map_losScreenTitle": "Sichtlinie",
|
||||||
|
"losSelectStartEnd": "Wählen Sie Start- und Endknoten für LOS aus.",
|
||||||
|
"losRunFailed": "Sichtlinienprüfung fehlgeschlagen: {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Löschen Sie alle Punkte",
|
||||||
|
"losRunToViewElevationProfile": "Führen Sie LOS aus, um das Höhenprofil anzuzeigen",
|
||||||
|
"losMenuTitle": "LOS-Menü",
|
||||||
|
"losMenuSubtitle": "Tippen Sie auf Knoten oder drücken Sie lange auf die Karte, um benutzerdefinierte Punkte anzuzeigen",
|
||||||
|
"losShowDisplayNodes": "Anzeigeknoten anzeigen",
|
||||||
|
"losCustomPoints": "Benutzerdefinierte Punkte",
|
||||||
|
"losCustomPointLabel": "Benutzerdefiniert {index}",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Punkt A",
|
||||||
|
"losPointB": "Punkt B",
|
||||||
|
"losAntennaA": "Antenne A: {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Antenne B: {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Führen Sie LOS aus",
|
||||||
|
"losNoElevationData": "Keine Höhendaten",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, freie Sichtlinie, Mindestabstand {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, blockiert durch {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "LOS: Überprüfen...",
|
||||||
|
"losStatusNoData": "LOS: keine Daten",
|
||||||
|
"losStatusSummary": "Sichtlinie: {clear}/{total} frei, {blocked} blockiert, {unknown} unbekannt",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Für eine oder mehrere Proben sind keine Höhendaten verfügbar.",
|
||||||
|
"losErrorInvalidInput": "Ungültige Punkte/Höhendaten für die LOS-Berechnung.",
|
||||||
|
"losRenameCustomPoint": "Benennen Sie den benutzerdefinierten Punkt um",
|
||||||
|
"losPointName": "Punktname",
|
||||||
|
"losShowPanelTooltip": "LOS-Panel anzeigen",
|
||||||
|
"losHidePanelTooltip": "LOS-Panel ausblenden",
|
||||||
|
"losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Funkhorizont",
|
||||||
|
"losLegendLosBeam": "Sichtlinie",
|
||||||
|
"losLegendTerrain": "Gelände",
|
||||||
|
"losFrequencyLabel": "Frequenz",
|
||||||
|
"losFrequencyInfoTooltip": "Details zur Berechnung anzeigen",
|
||||||
|
"losFrequencyDialogTitle": "Berechnung des Funkhorizonts",
|
||||||
|
"losFrequencyDialogDescription": "Ausgehend von k={baselineK} bei {baselineFreq} MHz passt die Berechnung den k-Faktor für das aktuelle {frequencyMHz} MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_unread": "Ungelesen",
|
||||||
|
"contacts_searchContactsNoNumber": "Kontakte suchen...",
|
||||||
|
"contacts_searchRepeaters": "Suche {number}{str} Repeater...",
|
||||||
|
"contacts_searchFavorites": "Suche {number}{str} Favoriten...",
|
||||||
|
"contacts_searchUsers": "Suche {number}{str} Benutzer...",
|
||||||
|
"contacts_searchRoomServers": "Suche {number}{str} Raumserver...",
|
||||||
|
"settings_contactSettings": "Kontakteinstellungen",
|
||||||
|
"contactsSettings_otherTitle": "Weitere Einstellungen zu Kontakten",
|
||||||
|
"contactsSettings_title": "Kontakteinstellungen",
|
||||||
|
"contactsSettings_autoAddTitle": "Automatische Erkennung",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Automatische Hinzufügung von Benutzern",
|
||||||
|
"settings_contactSettingsSubtitle": "Einstellungen für das Hinzufügen von Kontakten",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Automatisch Sensoren hinzufügen",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Benutzer hinzuzufügen",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Automatisch Raumservers hinzufügen",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Ermöglichen Sie dem Begleiter, entdeckte Raumserver automatisch hinzuzufügen",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Automatisch Repeater hinzufügen",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Repeater hinzuzufügen.",
|
||||||
|
"discoveredContacts_noMatching": "Keine passenden Kontakte",
|
||||||
|
"discoveredContacts_searchHint": "Entdeckte Kontakte suchen",
|
||||||
|
"discoveredContacts_addContact": "Kontakt hinzufügen",
|
||||||
|
"discoveredContacts_contactAdded": "Kontakt hinzugefügt",
|
||||||
|
"discoveredContacts_deleteContact": "Kontakt löschen",
|
||||||
|
"discoveredContacts_Title": "Entdeckte Kontakte",
|
||||||
|
"discoveredContacts_copyContact": "Kontakt in die Zwischenablage kopieren",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Überschreiben des Ältesten",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Sensoren hinzuzufügen",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Wenn die Kontaktliste voll ist, wird der älteste nicht favorisierte Kontakt ersetzt.",
|
||||||
|
"common_deleteAll": "Alles löschen",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Sind Sie sicher, dass Sie alle gefundenen Kontakte löschen möchten?",
|
||||||
|
"discoveredContacts_deleteContactAll": "Alle entdeckten Kontakte löschen",
|
||||||
|
"map_showGuessedLocations": "Zeige die vermuteten Knotenpositionen",
|
||||||
|
"map_guessedLocation": "Geschätzter Ort",
|
||||||
|
"usbScreenSubtitle": "Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenTitle": "Verbinden über USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenStatus": "Wählen Sie ein USB-Gerät aus",
|
||||||
|
"usbScreenNote": "Die USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.",
|
||||||
|
"usbScreenEmptyState": "Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie.",
|
||||||
|
"usbErrorPermissionDenied": "Die USB-Berechtigung wurde abgelehnt.",
|
||||||
|
"usbErrorDeviceMissing": "Das ausgewählte USB-Gerät ist nicht mehr verfügbar.",
|
||||||
|
"usbErrorInvalidPort": "Wählen Sie ein gültiges USB-Gerät aus.",
|
||||||
|
"usbErrorBusy": "Eine weitere Anfrage für eine USB-Verbindung ist bereits in Bearbeitung.",
|
||||||
|
"usbErrorNotConnected": "Es ist kein USB-Gerät angeschlossen.",
|
||||||
|
"usbErrorOpenFailed": "Fehlgeschlagen beim Öffnen des ausgewählten USB-Geräts.",
|
||||||
|
"usbErrorConnectFailed": "Keine Verbindung zum ausgewählten USB-Gerät hergestellt.",
|
||||||
|
"usbErrorUnsupported": "Die USB-Serielle Schnittstelle wird auf dieser Plattform nicht unterstützt.",
|
||||||
|
"usbErrorAlreadyActive": "Eine USB-Verbindung ist bereits hergestellt.",
|
||||||
|
"usbErrorNoDeviceSelected": "Kein USB-Gerät wurde ausgewählt.",
|
||||||
|
"usbErrorPortClosed": "Die USB-Verbindung ist nicht aktiv.",
|
||||||
|
"usbFallbackDeviceName": "Web-Serielle Geräte",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_searching": "Suche nach USB-Geräten...",
|
||||||
|
"usbStatus_notConnected": "Wählen Sie ein USB-Gerät aus",
|
||||||
|
"usbStatus_connecting": "Verbindung zum USB-Gerät...",
|
||||||
|
"usbConnectionFailed": "Fehler beim USB-Verbindungsaufbau: {error}",
|
||||||
|
"usbErrorConnectTimedOut": "Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält."
|
||||||
}
|
}
|
||||||
|
|||||||
+288
-15
@@ -10,6 +10,7 @@
|
|||||||
"common_unknownDevice": "Unknown Device",
|
"common_unknownDevice": "Unknown Device",
|
||||||
"common_save": "Save",
|
"common_save": "Save",
|
||||||
"common_delete": "Delete",
|
"common_delete": "Delete",
|
||||||
|
"common_deleteAll": "Delete All",
|
||||||
"common_close": "Close",
|
"common_close": "Close",
|
||||||
"common_edit": "Edit",
|
"common_edit": "Edit",
|
||||||
"common_add": "Add",
|
"common_add": "Add",
|
||||||
@@ -46,6 +47,37 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scanner_title": "MeshCore Open",
|
"scanner_title": "MeshCore Open",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenTitle": "Connect over USB",
|
||||||
|
"usbScreenSubtitle": "Choose a detected serial device and connect directly to your MeshCore node.",
|
||||||
|
"usbScreenStatus": "Select a USB device",
|
||||||
|
"usbScreenNote": "USB serial is active on supported Android devices and desktop platforms.",
|
||||||
|
"usbScreenEmptyState": "No USB devices found. Plug one in and refresh.",
|
||||||
|
"usbErrorPermissionDenied": "USB permission was denied.",
|
||||||
|
"usbErrorDeviceMissing": "The selected USB device is no longer available.",
|
||||||
|
"usbErrorInvalidPort": "Select a valid USB device.",
|
||||||
|
"usbErrorBusy": "Another USB connection request is already in progress.",
|
||||||
|
"usbErrorNotConnected": "No USB device is connected.",
|
||||||
|
"usbErrorOpenFailed": "Failed to open the selected USB device.",
|
||||||
|
"usbErrorConnectFailed": "Failed to connect to the selected USB device.",
|
||||||
|
"usbErrorUnsupported": "USB serial is not supported on this platform.",
|
||||||
|
"usbErrorAlreadyActive": "A USB connection is already active.",
|
||||||
|
"usbErrorNoDeviceSelected": "No USB device was selected.",
|
||||||
|
"usbErrorPortClosed": "The USB connection is not open.",
|
||||||
|
"usbErrorConnectTimedOut": "Connection timed out. Make sure the device has USB Companion firmware.",
|
||||||
|
"usbFallbackDeviceName": "Web Serial Device",
|
||||||
|
"usbStatus_notConnected": "Select a USB device",
|
||||||
|
"usbStatus_connecting": "Connecting to USB device...",
|
||||||
|
"usbStatus_searching": "Searching for USB devices...",
|
||||||
|
"usbConnectionFailed": "USB connection failed: {error}",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"scanner_scanning": "Scanning for devices...",
|
"scanner_scanning": "Scanning for devices...",
|
||||||
"scanner_connecting": "Connecting...",
|
"scanner_connecting": "Connecting...",
|
||||||
"scanner_disconnecting": "Disconnecting...",
|
"scanner_disconnecting": "Disconnecting...",
|
||||||
@@ -72,6 +104,8 @@
|
|||||||
"scanner_scan": "Scan",
|
"scanner_scan": "Scan",
|
||||||
"scanner_bluetoothOff": "Bluetooth is off",
|
"scanner_bluetoothOff": "Bluetooth is off",
|
||||||
"scanner_bluetoothOffMessage": "Please turn on Bluetooth to scan for devices",
|
"scanner_bluetoothOffMessage": "Please turn on Bluetooth to scan for devices",
|
||||||
|
"scanner_chromeRequired": "Chrome Browser Required",
|
||||||
|
"scanner_chromeRequiredMessage": "This web application requires Google Chrome or a Chromium-based browser for Bluetooth support.",
|
||||||
"scanner_enableBluetooth": "Enable Bluetooth",
|
"scanner_enableBluetooth": "Enable Bluetooth",
|
||||||
"device_quickSwitch": "Quick switch",
|
"device_quickSwitch": "Quick switch",
|
||||||
"device_meshcore": "MeshCore",
|
"device_meshcore": "MeshCore",
|
||||||
@@ -98,6 +132,8 @@
|
|||||||
"settings_locationIntervalInvalid": "Interval must be at least 60 seconds, and less than 86400 seconds.",
|
"settings_locationIntervalInvalid": "Interval must be at least 60 seconds, and less than 86400 seconds.",
|
||||||
"settings_latitude": "Latitude",
|
"settings_latitude": "Latitude",
|
||||||
"settings_longitude": "Longitude",
|
"settings_longitude": "Longitude",
|
||||||
|
"settings_contactSettings": "Contact Settings",
|
||||||
|
"settings_contactSettingsSubtitle": "Settings for how contacts are added.",
|
||||||
"settings_privacyMode": "Privacy Mode",
|
"settings_privacyMode": "Privacy Mode",
|
||||||
"settings_privacyModeSubtitle": "Hide name/location in advertisements",
|
"settings_privacyModeSubtitle": "Hide name/location in advertisements",
|
||||||
"settings_privacyModeToggle": "Toggle privacy mode to hide your name and location in advertisements.",
|
"settings_privacyModeToggle": "Toggle privacy mode to hide your name and location in advertisements.",
|
||||||
@@ -131,6 +167,7 @@
|
|||||||
},
|
},
|
||||||
"settings_aboutLegalese": "2026 MeshCore Open Source Project",
|
"settings_aboutLegalese": "2026 MeshCore Open Source Project",
|
||||||
"settings_aboutDescription": "An open-source Flutter client for MeshCore LoRa mesh networking devices.",
|
"settings_aboutDescription": "An open-source Flutter client for MeshCore LoRa mesh networking devices.",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "LOS elevation data: Open-Meteo (CC BY 4.0)",
|
||||||
"settings_infoName": "Name",
|
"settings_infoName": "Name",
|
||||||
"settings_infoId": "ID",
|
"settings_infoId": "ID",
|
||||||
"settings_infoStatus": "Status",
|
"settings_infoStatus": "Status",
|
||||||
@@ -182,6 +219,8 @@
|
|||||||
"appSettings_languageBg": "Български",
|
"appSettings_languageBg": "Български",
|
||||||
"appSettings_languageRu": "Русский",
|
"appSettings_languageRu": "Русский",
|
||||||
"appSettings_languageUk": "Українська",
|
"appSettings_languageUk": "Українська",
|
||||||
|
"appSettings_enableMessageTracing": "Enable Message Tracing",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Show detailed routing and timing metadata for messages",
|
||||||
"appSettings_notifications": "Notifications",
|
"appSettings_notifications": "Notifications",
|
||||||
"appSettings_enableNotifications": "Enable Notifications",
|
"appSettings_enableNotifications": "Enable Notifications",
|
||||||
"appSettings_enableNotificationsSubtitle": "Receive notifications for messages and adverts",
|
"appSettings_enableNotificationsSubtitle": "Receive notifications for messages and adverts",
|
||||||
@@ -242,6 +281,9 @@
|
|||||||
"appSettings_last24Hours": "Last 24 hours",
|
"appSettings_last24Hours": "Last 24 hours",
|
||||||
"appSettings_lastWeek": "Last week",
|
"appSettings_lastWeek": "Last week",
|
||||||
"appSettings_offlineMapCache": "Offline Map Cache",
|
"appSettings_offlineMapCache": "Offline Map Cache",
|
||||||
|
"appSettings_unitsTitle": "Units",
|
||||||
|
"appSettings_unitsMetric": "Metric (m / km)",
|
||||||
|
"appSettings_unitsImperial": "Imperial (ft / mi)",
|
||||||
"appSettings_noAreaSelected": "No area selected",
|
"appSettings_noAreaSelected": "No area selected",
|
||||||
"appSettings_areaSelectedZoom": "Area selected (zoom {minZoom}-{maxZoom})",
|
"appSettings_areaSelectedZoom": "Area selected (zoom {minZoom}-{maxZoom})",
|
||||||
"@appSettings_areaSelectedZoom": {
|
"@appSettings_areaSelectedZoom": {
|
||||||
@@ -262,7 +304,63 @@
|
|||||||
"contacts_title": "Contacts",
|
"contacts_title": "Contacts",
|
||||||
"contacts_noContacts": "No contacts yet",
|
"contacts_noContacts": "No contacts yet",
|
||||||
"contacts_contactsWillAppear": "Contacts will appear when devices advertise",
|
"contacts_contactsWillAppear": "Contacts will appear when devices advertise",
|
||||||
"contacts_searchContacts": "Search contacts...",
|
"contacts_unread": "Unread",
|
||||||
|
"contacts_searchContactsNoNumber": "Search Contacts...",
|
||||||
|
"contacts_searchContacts": "Search {number}{str} Contacts...",
|
||||||
|
"@contacts_searchContacts": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_searchFavorites": "Search {number}{str} Favorites...",
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_searchUsers": "Search {number}{str} Users...",
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_searchRepeaters": "Search {number}{str} Repeaters...",
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_searchRoomServers": "Search {number}{str} Room servers...",
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"contacts_noUnreadContacts": "No unread contacts",
|
"contacts_noUnreadContacts": "No unread contacts",
|
||||||
"contacts_noContactsFound": "No contacts or groups found",
|
"contacts_noContactsFound": "No contacts or groups found",
|
||||||
"contacts_deleteContact": "Delete Contact",
|
"contacts_deleteContact": "Delete Contact",
|
||||||
@@ -302,8 +400,8 @@
|
|||||||
"contacts_filterContacts": "Filter contacts...",
|
"contacts_filterContacts": "Filter contacts...",
|
||||||
"contacts_noContactsMatchFilter": "No contacts match your filter",
|
"contacts_noContactsMatchFilter": "No contacts match your filter",
|
||||||
"contacts_noMembers": "No members",
|
"contacts_noMembers": "No members",
|
||||||
"contacts_lastSeenNow": "Last seen now",
|
"contacts_lastSeenNow": "recently",
|
||||||
"contacts_lastSeenMinsAgo": "Last seen {minutes} mins ago",
|
"contacts_lastSeenMinsAgo": "~ {minutes} min.",
|
||||||
"@contacts_lastSeenMinsAgo": {
|
"@contacts_lastSeenMinsAgo": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"minutes": {
|
"minutes": {
|
||||||
@@ -311,8 +409,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contacts_lastSeenHourAgo": "Last seen 1 hour ago",
|
"contacts_lastSeenHourAgo": "~ 1 hour",
|
||||||
"contacts_lastSeenHoursAgo": "Last seen {hours} hours ago",
|
"contacts_lastSeenHoursAgo": "~ {hours} hours",
|
||||||
"@contacts_lastSeenHoursAgo": {
|
"@contacts_lastSeenHoursAgo": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"hours": {
|
"hours": {
|
||||||
@@ -320,8 +418,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contacts_lastSeenDayAgo": "Last seen 1 day ago",
|
"contacts_lastSeenDayAgo": "~ 1 day",
|
||||||
"contacts_lastSeenDaysAgo": "Last seen {days} days ago",
|
"contacts_lastSeenDaysAgo": "~ {days} days",
|
||||||
"@contacts_lastSeenDaysAgo": {
|
"@contacts_lastSeenDaysAgo": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"days": {
|
"days": {
|
||||||
@@ -348,6 +446,8 @@
|
|||||||
"channels_publicChannel": "Public channel",
|
"channels_publicChannel": "Public channel",
|
||||||
"channels_privateChannel": "Private channel",
|
"channels_privateChannel": "Private channel",
|
||||||
"channels_editChannel": "Edit channel",
|
"channels_editChannel": "Edit channel",
|
||||||
|
"channels_muteChannel": "Mute channel",
|
||||||
|
"channels_unmuteChannel": "Unmute channel",
|
||||||
"channels_deleteChannel": "Delete channel",
|
"channels_deleteChannel": "Delete channel",
|
||||||
"channels_deleteChannelConfirm": "Delete \"{name}\"? This cannot be undone.",
|
"channels_deleteChannelConfirm": "Delete \"{name}\"? This cannot be undone.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
@@ -357,6 +457,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"channels_channelDeleteFailed": "Failed to delete channel \"{name}\"",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"channels_channelDeleted": "Channel \"{name}\" deleted",
|
"channels_channelDeleted": "Channel \"{name}\" deleted",
|
||||||
"@channels_channelDeleted": {
|
"@channels_channelDeleted": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -558,6 +666,7 @@
|
|||||||
},
|
},
|
||||||
"debugFrame_hexDump": "Hex Dump:",
|
"debugFrame_hexDump": "Hex Dump:",
|
||||||
"chat_pathManagement": "Path Management",
|
"chat_pathManagement": "Path Management",
|
||||||
|
"chat_ShowAllPaths": "Show all paths",
|
||||||
"chat_routingMode": "Routing mode",
|
"chat_routingMode": "Routing mode",
|
||||||
"chat_autoUseSavedPath": "Auto (use saved path)",
|
"chat_autoUseSavedPath": "Auto (use saved path)",
|
||||||
"chat_forceFloodMode": "Force Flood Mode",
|
"chat_forceFloodMode": "Force Flood Mode",
|
||||||
@@ -638,6 +747,8 @@
|
|||||||
},
|
},
|
||||||
"chat_invalidLink": "Invalid link format",
|
"chat_invalidLink": "Invalid link format",
|
||||||
"map_title": "Node Map",
|
"map_title": "Node Map",
|
||||||
|
"map_lineOfSight": "Line of Sight",
|
||||||
|
"map_losScreenTitle": "Line of Sight",
|
||||||
"map_noNodesWithLocation": "No nodes with location data",
|
"map_noNodesWithLocation": "No nodes with location data",
|
||||||
"map_nodesNeedGps": "Nodes need to share their GPS coordinates\nto appear on the map",
|
"map_nodesNeedGps": "Nodes need to share their GPS coordinates\nto appear on the map",
|
||||||
"map_nodesCount": "Nodes: {count}",
|
"map_nodesCount": "Nodes: {count}",
|
||||||
@@ -695,6 +806,8 @@
|
|||||||
"map_publicKeyPrefix": "Public key prefix",
|
"map_publicKeyPrefix": "Public key prefix",
|
||||||
"map_markers": "Markers",
|
"map_markers": "Markers",
|
||||||
"map_showSharedMarkers": "Show shared markers",
|
"map_showSharedMarkers": "Show shared markers",
|
||||||
|
"map_showGuessedLocations": "Show guessed node locations",
|
||||||
|
"map_guessedLocation": "Guessed location",
|
||||||
"map_lastSeenTime": "Last Seen Time",
|
"map_lastSeenTime": "Last Seen Time",
|
||||||
"map_sharedPin": "Shared pin",
|
"map_sharedPin": "Shared pin",
|
||||||
"map_joinRoom": "Join Room",
|
"map_joinRoom": "Join Room",
|
||||||
@@ -905,8 +1018,8 @@
|
|||||||
"repeater_telemetrySubtitle": "View telemetry of sensors and system stats",
|
"repeater_telemetrySubtitle": "View telemetry of sensors and system stats",
|
||||||
"repeater_cli": "CLI",
|
"repeater_cli": "CLI",
|
||||||
"repeater_cliSubtitle": "Send commands to the repeater",
|
"repeater_cliSubtitle": "Send commands to the repeater",
|
||||||
"repeater_neighbours": "Neighbors",
|
"repeater_neighbors": "Neighbors",
|
||||||
"repeater_neighboursSubtitle": "View zero hop neighbors.",
|
"repeater_neighborsSubtitle": "View zero hop neighbors.",
|
||||||
"repeater_settings": "Settings",
|
"repeater_settings": "Settings",
|
||||||
"repeater_settingsSubtitle": "Configure repeater parameters",
|
"repeater_settingsSubtitle": "Configure repeater parameters",
|
||||||
"repeater_statusTitle": "Repeater Status",
|
"repeater_statusTitle": "Repeater Status",
|
||||||
@@ -1266,8 +1379,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"neighbors_receivedData": "Received Neighbours Data",
|
"neighbors_receivedData": "Received Neighbors Data",
|
||||||
"neighbors_requestTimedOut": "Neighbours request timed out.",
|
"neighbors_requestTimedOut": "Neighbors request timed out.",
|
||||||
"neighbors_errorLoading": "Error loading neighbors: {error}",
|
"neighbors_errorLoading": "Error loading neighbors: {error}",
|
||||||
"@neighbors_errorLoading": {
|
"@neighbors_errorLoading": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1276,8 +1389,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"neighbors_repeatersNeighbours": "Repeaters Neighbours",
|
"neighbors_repeatersNeighbors": "Repeaters Neighbors",
|
||||||
"neighbors_noData": "No neighbours data available.",
|
"neighbors_noData": "No neighbors data available.",
|
||||||
"neighbors_unknownContact": "Unknown {pubkey}",
|
"neighbors_unknownContact": "Unknown {pubkey}",
|
||||||
"@neighbors_unknownContact": {
|
"@neighbors_unknownContact": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1536,6 +1649,9 @@
|
|||||||
"listFilter_az": "A-Z",
|
"listFilter_az": "A-Z",
|
||||||
"listFilter_filters": "Filters",
|
"listFilter_filters": "Filters",
|
||||||
"listFilter_all": "All",
|
"listFilter_all": "All",
|
||||||
|
"listFilter_favorites": "Favorites",
|
||||||
|
"listFilter_addToFavorites": "Add to favorites",
|
||||||
|
"listFilter_removeFromFavorites": "Remove from favorites",
|
||||||
"listFilter_users": "Users",
|
"listFilter_users": "Users",
|
||||||
"listFilter_repeaters": "Repeaters",
|
"listFilter_repeaters": "Repeaters",
|
||||||
"listFilter_roomServers": "Room servers",
|
"listFilter_roomServers": "Room servers",
|
||||||
@@ -1547,6 +1663,139 @@
|
|||||||
"pathTrace_refreshTooltip": "Refresh Path Trace.",
|
"pathTrace_refreshTooltip": "Refresh Path Trace.",
|
||||||
"pathTrace_someHopsNoLocation": "One or more of the hops is missing a location!",
|
"pathTrace_someHopsNoLocation": "One or more of the hops is missing a location!",
|
||||||
"pathTrace_clearTooltip": "Clear path.",
|
"pathTrace_clearTooltip": "Clear path.",
|
||||||
|
"losSelectStartEnd": "Select start and end nodes for LOS.",
|
||||||
|
"losRunFailed": "Line-of-sight check failed: {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Clear all points",
|
||||||
|
"losRunToViewElevationProfile": "Run LOS to view elevation profile",
|
||||||
|
"losMenuTitle": "LOS Menu",
|
||||||
|
"losMenuSubtitle": "Tap nodes or long-press map for custom points",
|
||||||
|
"losShowDisplayNodes": "Show display nodes",
|
||||||
|
"losCustomPoints": "Custom points",
|
||||||
|
"losCustomPointLabel": "Custom {index}",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Point A",
|
||||||
|
"losPointB": "Point B",
|
||||||
|
"losAntennaA": "Antenna A: {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Antenna B: {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Run LOS",
|
||||||
|
"losNoElevationData": "No elevation data",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, clear LOS, min clearance {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, blocked by {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "LOS: checking...",
|
||||||
|
"losStatusNoData": "LOS: no data",
|
||||||
|
"losStatusSummary": "LOS: {clear}/{total} clear, {blocked} blocked, {unknown} unknown",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Elevation data unavailable for one or more samples.",
|
||||||
|
"losErrorInvalidInput": "Invalid points/elevation data for LOS calculation.",
|
||||||
|
"losRenameCustomPoint": "Rename custom point",
|
||||||
|
"losPointName": "Point name",
|
||||||
|
"losShowPanelTooltip": "Show LOS panel",
|
||||||
|
"losHidePanelTooltip": "Hide LOS panel",
|
||||||
|
"losElevationAttribution": "Elevation data: Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Radio horizon",
|
||||||
|
"losLegendLosBeam": "LOS beam",
|
||||||
|
"losLegendTerrain": "Terrain",
|
||||||
|
"losFrequencyLabel": "Frequency",
|
||||||
|
"losFrequencyInfoTooltip": "View calculation details",
|
||||||
|
"losFrequencyDialogTitle": "Radio horizon calculation",
|
||||||
|
"losFrequencyDialogDescription": "Starting from k={baselineK} at {baselineFreq} MHz, the calculation adjusts the k-factor for the current {frequencyMHz} MHz band, which defines the curved radio horizon cap.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"contacts_pathTrace": "Path Trace",
|
"contacts_pathTrace": "Path Trace",
|
||||||
"contacts_ping": "Ping",
|
"contacts_ping": "Ping",
|
||||||
"contacts_repeaterPathTrace": "Path trace to repeater",
|
"contacts_repeaterPathTrace": "Path trace to repeater",
|
||||||
@@ -1624,5 +1873,29 @@
|
|||||||
"settings_gpxExportChat": "Companion locations",
|
"settings_gpxExportChat": "Companion locations",
|
||||||
"settings_gpxExportAllContacts": "All contacts locations",
|
"settings_gpxExportAllContacts": "All contacts locations",
|
||||||
"settings_gpxExportShareText": "Map data exported from meshcore-open",
|
"settings_gpxExportShareText": "Map data exported from meshcore-open",
|
||||||
"settings_gpxExportShareSubject": "meshcore-open GPX map data export"
|
"settings_gpxExportShareSubject": "meshcore-open GPX map data export",
|
||||||
}
|
"snrIndicator_nearByRepeaters": "Nearby Repeaters",
|
||||||
|
"snrIndicator_lastSeen": "Last seen",
|
||||||
|
"contactsSettings_title": "Contacts settings",
|
||||||
|
"contactsSettings_autoAddTitle": "Automatic Discovery",
|
||||||
|
"contactsSettings_otherTitle": "Other contact related settings",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Auto-add users",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Allow the companion to automatically add discovered users.",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Auto-add repeaters",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Allow the companion to automatically add discovered repeaters.",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Auto-add room servers",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Allow the companion to automatically add discovered room servers.",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Auto-add sensors",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Allow the companion to automatically add discovered sensors.",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Overwrite Oldest",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "When the contact list is full, the oldest non-favorited contact will be replaced.",
|
||||||
|
"discoveredContacts_Title": "Discovered Contacts",
|
||||||
|
"discoveredContacts_noMatching": "No matching contacts",
|
||||||
|
"discoveredContacts_searchHint": "Search discovered contacts",
|
||||||
|
"discoveredContacts_contactAdded": "Contact added",
|
||||||
|
"discoveredContacts_addContact": "Add Contact",
|
||||||
|
"discoveredContacts_copyContact": "Copy Contact to clipboard",
|
||||||
|
"discoveredContacts_deleteContact": "Delete Discovered Contact",
|
||||||
|
"discoveredContacts_deleteContactAll": "Delete All Discovered Contacts",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Are you sure you want to delete all discovered contacts?"
|
||||||
|
}
|
||||||
+272
-9
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"channels_channelDeleteFailed": "No se pudo eliminar el canal \"{name}\"",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@@locale": "es",
|
"@@locale": "es",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Contactos",
|
"nav_contacts": "Contactos",
|
||||||
@@ -289,7 +297,7 @@
|
|||||||
"contacts_noContactsMatchFilter": "No hay contactos que coincidan con tu filtro",
|
"contacts_noContactsMatchFilter": "No hay contactos que coincidan con tu filtro",
|
||||||
"contacts_noMembers": "No miembros",
|
"contacts_noMembers": "No miembros",
|
||||||
"contacts_lastSeenNow": "Última vez que se vio ahora",
|
"contacts_lastSeenNow": "Última vez que se vio ahora",
|
||||||
"contacts_lastSeenMinsAgo": "Última vez visto hace {minutes} minutos.",
|
"contacts_lastSeenMinsAgo": "~ {minutes} min.",
|
||||||
"@contacts_lastSeenMinsAgo": {
|
"@contacts_lastSeenMinsAgo": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"minutes": {
|
"minutes": {
|
||||||
@@ -297,8 +305,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contacts_lastSeenHourAgo": "Última vez que se vio hace 1 hora",
|
"contacts_lastSeenHourAgo": "~ 1 hora",
|
||||||
"contacts_lastSeenHoursAgo": "Última vez visto hace {hours} horas.",
|
"contacts_lastSeenHoursAgo": "~ {hours} horas",
|
||||||
"@contacts_lastSeenHoursAgo": {
|
"@contacts_lastSeenHoursAgo": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"hours": {
|
"hours": {
|
||||||
@@ -306,8 +314,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contacts_lastSeenDayAgo": "Última vez que se vio hace 1 día",
|
"contacts_lastSeenDayAgo": "~ 1 día",
|
||||||
"contacts_lastSeenDaysAgo": "Última vez visto hace {days} días.",
|
"contacts_lastSeenDaysAgo": "~ {days} días",
|
||||||
"@contacts_lastSeenDaysAgo": {
|
"@contacts_lastSeenDaysAgo": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"days": {
|
"days": {
|
||||||
@@ -334,6 +342,8 @@
|
|||||||
"channels_publicChannel": "Canal público",
|
"channels_publicChannel": "Canal público",
|
||||||
"channels_privateChannel": "Canal privado",
|
"channels_privateChannel": "Canal privado",
|
||||||
"channels_editChannel": "Editar canal",
|
"channels_editChannel": "Editar canal",
|
||||||
|
"channels_muteChannel": "Silenciar canal",
|
||||||
|
"channels_unmuteChannel": "Activar canal",
|
||||||
"channels_deleteChannel": "Eliminar canal",
|
"channels_deleteChannel": "Eliminar canal",
|
||||||
"channels_deleteChannelConfirm": "Eliminar \"{name}\"? Esto no se puede deshacer.",
|
"channels_deleteChannelConfirm": "Eliminar \"{name}\"? Esto no se puede deshacer.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
@@ -1351,12 +1361,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Vecinos",
|
"repeater_neighbors": "Vecinos",
|
||||||
"repeater_neighboursSubtitle": "Ver vecinos de salto cero.",
|
"repeater_neighborsSubtitle": "Ver vecinos de salto cero.",
|
||||||
"neighbors_receivedData": "Recibidas Datos de Vecinos",
|
"neighbors_receivedData": "Recibidas Datos de Vecinos",
|
||||||
"neighbors_requestTimedOut": "Los vecinos solicitan que se desconecte.",
|
"neighbors_requestTimedOut": "Los vecinos solicitan que se desconecte.",
|
||||||
"neighbors_errorLoading": "Error al cargar vecinos: {error}",
|
"neighbors_errorLoading": "Error al cargar vecinos: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Repetidores Vecinos",
|
"neighbors_repeatersNeighbors": "Repetidores Vecinos",
|
||||||
"neighbors_noData": "No hay datos de vecinos disponibles.",
|
"neighbors_noData": "No hay datos de vecinos disponibles.",
|
||||||
"channels_joinPrivateChannel": "Únete a un Canal Privado",
|
"channels_joinPrivateChannel": "Únete a un Canal Privado",
|
||||||
"channels_createPrivateChannel": "Crear un Canal Privado",
|
"channels_createPrivateChannel": "Crear un Canal Privado",
|
||||||
@@ -1551,6 +1561,8 @@
|
|||||||
"appSettings_languageUk": "Ucraniano",
|
"appSettings_languageUk": "Ucraniano",
|
||||||
"contacts_clipboardEmpty": "El portapapeles está vacío.",
|
"contacts_clipboardEmpty": "El portapapeles está vacío.",
|
||||||
"appSettings_languageRu": "Ruso",
|
"appSettings_languageRu": "Ruso",
|
||||||
|
"appSettings_enableMessageTracing": "Habilitar seguimiento de mensajes",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Mostrar metadatos detallados de enrutamiento y tiempo para los mensajes",
|
||||||
"contacts_invalidAdvertFormat": "Datos de contacto no válidos",
|
"contacts_invalidAdvertFormat": "Datos de contacto no válidos",
|
||||||
"contacts_floodAdvert": "Anuncio de inundación",
|
"contacts_floodAdvert": "Anuncio de inundación",
|
||||||
"contacts_contactImported": "El contacto ha sido importado.",
|
"contacts_contactImported": "El contacto ha sido importado.",
|
||||||
@@ -1620,9 +1632,260 @@
|
|||||||
"map_removeLast": "Eliminar último",
|
"map_removeLast": "Eliminar último",
|
||||||
"map_pathTraceCancelled": "Rastreo de ruta cancelado.",
|
"map_pathTraceCancelled": "Rastreo de ruta cancelado.",
|
||||||
"scanner_bluetoothOffMessage": "Por favor, active el Bluetooth para escanear dispositivos.",
|
"scanner_bluetoothOffMessage": "Por favor, active el Bluetooth para escanear dispositivos.",
|
||||||
|
"scanner_chromeRequired": "Navegador Chrome requerido",
|
||||||
|
"scanner_chromeRequiredMessage": "Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.",
|
||||||
"scanner_bluetoothOff": "Bluetooth está desactivado.",
|
"scanner_bluetoothOff": "Bluetooth está desactivado.",
|
||||||
"scanner_enableBluetooth": "Habilitar Bluetooth",
|
"scanner_enableBluetooth": "Habilitar Bluetooth",
|
||||||
|
"snrIndicator_nearByRepeaters": "Repetidores cercanos",
|
||||||
|
"snrIndicator_lastSeen": "Visto por última vez",
|
||||||
|
"chat_ShowAllPaths": "Mostrar todos los caminos",
|
||||||
"settings_clientRepeatFreqWarning": "Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.",
|
"settings_clientRepeatFreqWarning": "Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.",
|
||||||
"settings_clientRepeat": "Repetir sin conexión",
|
"settings_clientRepeat": "Repetir sin conexión",
|
||||||
"settings_clientRepeatSubtitle": "Permita que este dispositivo repita los paquetes de red para otros usuarios."
|
"settings_clientRepeatSubtitle": "Permita que este dispositivo repita los paquetes de red para otros usuarios.",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "Datos de elevación LOS: Open-Meteo (CC BY 4.0)",
|
||||||
|
"appSettings_unitsTitle": "Unidades",
|
||||||
|
"appSettings_unitsMetric": "Métrico (m/km)",
|
||||||
|
"appSettings_unitsImperial": "Imperial (pies/millas)",
|
||||||
|
"map_lineOfSight": "Línea de visión",
|
||||||
|
"map_losScreenTitle": "Línea de visión",
|
||||||
|
"losSelectStartEnd": "Seleccione los nodos de inicio y fin para LOS.",
|
||||||
|
"losRunFailed": "Error en la comprobación de la línea de visión: {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Borrar todos los puntos",
|
||||||
|
"losRunToViewElevationProfile": "Ejecute LOS para ver el perfil de elevación",
|
||||||
|
"losMenuTitle": "Menú LOS",
|
||||||
|
"losMenuSubtitle": "Toque nodos o mantenga presionado el mapa para puntos personalizados",
|
||||||
|
"losShowDisplayNodes": "Mostrar nodos de visualización",
|
||||||
|
"losCustomPoints": "Puntos personalizados",
|
||||||
|
"losCustomPointLabel": "Personalizado {index}",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Punto A",
|
||||||
|
"losPointB": "Punto B",
|
||||||
|
"losAntennaA": "Antena A: {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Antena B: {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Ejecutar LOS",
|
||||||
|
"losNoElevationData": "Sin datos de elevación",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, despejar LOS, autorización mínima {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, bloqueado por {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "LOS: comprobando...",
|
||||||
|
"losStatusNoData": "LOS: sin datos",
|
||||||
|
"losStatusSummary": "LOS: {clear}/{total} claro, {blocked} bloqueado, {unknown} desconocido",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Datos de elevación no disponibles para una o más muestras.",
|
||||||
|
"losErrorInvalidInput": "Datos de puntos/elevación no válidos para el cálculo de LOS.",
|
||||||
|
"losRenameCustomPoint": "Cambiar el nombre del punto personalizado",
|
||||||
|
"losPointName": "Nombre del punto",
|
||||||
|
"losShowPanelTooltip": "Mostrar panel LOS",
|
||||||
|
"losHidePanelTooltip": "Ocultar panel LOS",
|
||||||
|
"losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Horizonte radioeléctrico",
|
||||||
|
"losLegendLosBeam": "Línea de visión",
|
||||||
|
"losLegendTerrain": "Terreno",
|
||||||
|
"losFrequencyLabel": "Frecuencia",
|
||||||
|
"losFrequencyInfoTooltip": "Ver detalles del cálculo",
|
||||||
|
"losFrequencyDialogTitle": "Cálculo del horizonte radioeléctrico",
|
||||||
|
"losFrequencyDialogDescription": "A partir de k={baselineK} en {baselineFreq} MHz, el cálculo ajusta el factor k para la banda actual de {frequencyMHz} MHz, que define el límite curvo del horizonte de radio.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listFilter_favorites": "Favoritos",
|
||||||
|
"listFilter_removeFromFavorites": "Eliminar de las favoritas",
|
||||||
|
"listFilter_addToFavorites": "Añadir a favoritos",
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_searchContactsNoNumber": "Buscar contactos...",
|
||||||
|
"contacts_unread": "No leído",
|
||||||
|
"contacts_searchFavorites": "Buscar {number}{str} Favoritos...",
|
||||||
|
"contacts_searchUsers": "Buscar {number}{str} Usuarios...",
|
||||||
|
"contacts_searchRepeaters": "Buscar {number}{str} Repetidores...",
|
||||||
|
"contacts_searchRoomServers": "Buscar {number}{str} servidores de sala...",
|
||||||
|
"contactsSettings_autoAddTitle": "Detección automática",
|
||||||
|
"settings_contactSettings": "Configuración de contacto",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Agregar usuarios automáticamente",
|
||||||
|
"contactsSettings_otherTitle": "Otras configuraciones relacionadas con el contacto",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Permitir que el compañero agregue automáticamente a los usuarios descubiertos.",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Permitir que el compañero agregue automáticamente los repetidores descubiertos.",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Permitir que el compañero agregue automáticamente los servidores de salas descubiertos.",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Agregar sensores automáticamente",
|
||||||
|
"contactsSettings_title": "Configuración de contactos",
|
||||||
|
"settings_contactSettingsSubtitle": "Configuración de cómo se agregan los contactos.",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Permitir que el compañero agregue automáticamente los sensores descubiertos.",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Agregar repetidores automáticamente",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Sobreescribir el más antiguo",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Agregar automáticamente servidores de sala",
|
||||||
|
"discoveredContacts_noMatching": "No se encontraron contactos coincidentes",
|
||||||
|
"discoveredContacts_contactAdded": "Contacto agregado",
|
||||||
|
"discoveredContacts_copyContact": "Copiar contacto al portapapeles",
|
||||||
|
"discoveredContacts_deleteContact": "Eliminar contacto",
|
||||||
|
"discoveredContacts_Title": "Contactos descubiertos",
|
||||||
|
"discoveredContacts_searchHint": "Buscar contactos descubiertos",
|
||||||
|
"discoveredContacts_addContact": "Agregar contacto",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Cuando la lista de contactos esté llena, se reemplazará el contacto no favorito más antiguo.",
|
||||||
|
"common_deleteAll": "Eliminar todo",
|
||||||
|
"discoveredContacts_deleteContactAll": "Eliminar Todos los Contactos Descubiertos",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "¿Está seguro de que desea eliminar todos los contactos descubiertos!",
|
||||||
|
"map_guessedLocation": "Ubicación estimada",
|
||||||
|
"map_showGuessedLocations": "Mostrar las ubicaciones estimadas de los nodos.",
|
||||||
|
"usbScreenTitle": "Conecte mediante USB",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenSubtitle": "Seleccione el dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.",
|
||||||
|
"usbScreenStatus": "Seleccione un dispositivo USB",
|
||||||
|
"usbScreenNote": "La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.",
|
||||||
|
"usbScreenEmptyState": "No se encontraron dispositivos USB. Conecte uno y vuelva a intentar.",
|
||||||
|
"usbErrorPermissionDenied": "Se denegó el permiso de acceso a través de USB.",
|
||||||
|
"usbErrorDeviceMissing": "El dispositivo USB seleccionado ya no está disponible.",
|
||||||
|
"usbErrorInvalidPort": "Seleccione un dispositivo USB válido.",
|
||||||
|
"usbErrorBusy": "Ya se ha iniciado una solicitud de conexión USB adicional.",
|
||||||
|
"usbErrorNotConnected": "No hay ningún dispositivo USB conectado.",
|
||||||
|
"usbErrorOpenFailed": "No se pudo abrir el dispositivo USB seleccionado.",
|
||||||
|
"usbErrorConnectFailed": "No se pudo conectar con el dispositivo USB seleccionado.",
|
||||||
|
"usbErrorUnsupported": "La comunicación serial a través de USB no está soportada en esta plataforma.",
|
||||||
|
"usbErrorAlreadyActive": "La conexión USB ya está activa.",
|
||||||
|
"usbErrorNoDeviceSelected": "No se ha seleccionado ningún dispositivo USB.",
|
||||||
|
"usbErrorPortClosed": "La conexión USB no está activa.",
|
||||||
|
"usbFallbackDeviceName": "Dispositivo de serie web",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_connecting": "Conectándose al dispositivo USB...",
|
||||||
|
"usbStatus_searching": "Buscando dispositivos USB...",
|
||||||
|
"usbStatus_notConnected": "Seleccione un dispositivo USB",
|
||||||
|
"usbConnectionFailed": "Error al conectar mediante USB: {error}",
|
||||||
|
"usbErrorConnectTimedOut": "La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion."
|
||||||
}
|
}
|
||||||
|
|||||||
+275
-12
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"channels_channelDeleteFailed": "Échec de la suppression de la chaîne \"{name}\"",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@@locale": "fr",
|
"@@locale": "fr",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Contacts",
|
"nav_contacts": "Contacts",
|
||||||
@@ -262,7 +270,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contacts_manageRepeater": "Gérer le répéteur",
|
"contacts_manageRepeater": "Gérer le répéteur",
|
||||||
"contacts_roomLogin": "Connexion Salle",
|
"contacts_roomLogin": "Connexion Room Server",
|
||||||
"contacts_openChat": "Ouverture du Chat",
|
"contacts_openChat": "Ouverture du Chat",
|
||||||
"contacts_editGroup": "Modifier le groupe",
|
"contacts_editGroup": "Modifier le groupe",
|
||||||
"contacts_deleteGroup": "Supprimer le groupe",
|
"contacts_deleteGroup": "Supprimer le groupe",
|
||||||
@@ -289,7 +297,7 @@
|
|||||||
"contacts_noContactsMatchFilter": "Aucun contact ne correspond à votre filtre.",
|
"contacts_noContactsMatchFilter": "Aucun contact ne correspond à votre filtre.",
|
||||||
"contacts_noMembers": "Aucun membre",
|
"contacts_noMembers": "Aucun membre",
|
||||||
"contacts_lastSeenNow": "Vu maintenant",
|
"contacts_lastSeenNow": "Vu maintenant",
|
||||||
"contacts_lastSeenMinsAgo": "Vu il y a {minutes} minutes",
|
"contacts_lastSeenMinsAgo": "~ {minutes} min.",
|
||||||
"@contacts_lastSeenMinsAgo": {
|
"@contacts_lastSeenMinsAgo": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"minutes": {
|
"minutes": {
|
||||||
@@ -297,8 +305,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contacts_lastSeenHourAgo": "Vu il y a 1 heure",
|
"contacts_lastSeenHourAgo": "~ 1 heure",
|
||||||
"contacts_lastSeenHoursAgo": "Vu il y a {hours} heures",
|
"contacts_lastSeenHoursAgo": "~ {hours} heures",
|
||||||
"@contacts_lastSeenHoursAgo": {
|
"@contacts_lastSeenHoursAgo": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"hours": {
|
"hours": {
|
||||||
@@ -306,8 +314,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contacts_lastSeenDayAgo": "Vu il y a 1 jour",
|
"contacts_lastSeenDayAgo": "~ 1 jour",
|
||||||
"contacts_lastSeenDaysAgo": "Vu il y a {days} jours",
|
"contacts_lastSeenDaysAgo": "~ {days} jours",
|
||||||
"@contacts_lastSeenDaysAgo": {
|
"@contacts_lastSeenDaysAgo": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"days": {
|
"days": {
|
||||||
@@ -334,6 +342,8 @@
|
|||||||
"channels_publicChannel": "Canal public",
|
"channels_publicChannel": "Canal public",
|
||||||
"channels_privateChannel": "Canal privé",
|
"channels_privateChannel": "Canal privé",
|
||||||
"channels_editChannel": "Modifier le canal",
|
"channels_editChannel": "Modifier le canal",
|
||||||
|
"channels_muteChannel": "Désactiver les notifications du canal",
|
||||||
|
"channels_unmuteChannel": "Réactiver les notifications du canal",
|
||||||
"channels_deleteChannel": "Supprimer le canal",
|
"channels_deleteChannel": "Supprimer le canal",
|
||||||
"channels_deleteChannelConfirm": "Supprimer {name}? Cela ne peut pas être annulé.",
|
"channels_deleteChannelConfirm": "Supprimer {name}? Cela ne peut pas être annulé.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
@@ -796,7 +806,7 @@
|
|||||||
"dialog_disconnect": "Déconnecter",
|
"dialog_disconnect": "Déconnecter",
|
||||||
"dialog_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?",
|
"dialog_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?",
|
||||||
"login_repeaterLogin": "Connexion au répéteur",
|
"login_repeaterLogin": "Connexion au répéteur",
|
||||||
"login_roomLogin": "Connexion Salle",
|
"login_roomLogin": "Connexion Room Server",
|
||||||
"login_password": "Mot de passe",
|
"login_password": "Mot de passe",
|
||||||
"login_enterPassword": "Entrez votre mot de passe",
|
"login_enterPassword": "Entrez votre mot de passe",
|
||||||
"login_savePassword": "Sauvegarder le mot de passe",
|
"login_savePassword": "Sauvegarder le mot de passe",
|
||||||
@@ -1351,12 +1361,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Voisins",
|
"repeater_neighbors": "Voisins",
|
||||||
"repeater_neighboursSubtitle": "Afficher les voisins de saut nuls.",
|
"repeater_neighborsSubtitle": "Afficher les voisins de saut nuls.",
|
||||||
"neighbors_receivedData": "Données des voisins reçues",
|
"neighbors_receivedData": "Données des voisins reçues",
|
||||||
"neighbors_requestTimedOut": "Les voisins demandent un délai.",
|
"neighbors_requestTimedOut": "Les voisins demandent un délai.",
|
||||||
"neighbors_errorLoading": "Erreur lors du chargement des voisins : {error}",
|
"neighbors_errorLoading": "Erreur lors du chargement des voisins : {error}",
|
||||||
"neighbors_repeatersNeighbours": "Répéteurs Voisins",
|
"neighbors_repeatersNeighbors": "Répéteurs Voisins",
|
||||||
"neighbors_noData": "Aucune donnée concernant les voisins disponible.",
|
"neighbors_noData": "Aucune donnée concernant les voisins disponible.",
|
||||||
"channels_createPrivateChannelDesc": "Sécurisé avec une clé secrète.",
|
"channels_createPrivateChannelDesc": "Sécurisé avec une clé secrète.",
|
||||||
"channels_joinPrivateChannel": "Rejoindre un Canal Privé",
|
"channels_joinPrivateChannel": "Rejoindre un Canal Privé",
|
||||||
@@ -1391,7 +1401,7 @@
|
|||||||
"settings_locationIntervalSec": "Intervalle de mise-à-jour du GPS (Secondes)",
|
"settings_locationIntervalSec": "Intervalle de mise-à-jour du GPS (Secondes)",
|
||||||
"settings_locationIntervalInvalid": "L'intervalle doit être compris entre 60 et 86400 secondes.",
|
"settings_locationIntervalInvalid": "L'intervalle doit être compris entre 60 et 86400 secondes.",
|
||||||
"contacts_manageRoom": "Gérer le Room Server",
|
"contacts_manageRoom": "Gérer le Room Server",
|
||||||
"room_management": "Administración del Servidor de Habitación",
|
"room_management": "Administrattion Room Server",
|
||||||
"@community_joinConfirmation": {
|
"@community_joinConfirmation": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"name": {
|
"name": {
|
||||||
@@ -1551,6 +1561,8 @@
|
|||||||
"contacts_invalidAdvertFormat": "Données de contact non valides",
|
"contacts_invalidAdvertFormat": "Données de contact non valides",
|
||||||
"appSettings_languageUk": "Ukrainien",
|
"appSettings_languageUk": "Ukrainien",
|
||||||
"appSettings_languageRu": "Russe",
|
"appSettings_languageRu": "Russe",
|
||||||
|
"appSettings_enableMessageTracing": "Activer le traçage des messages",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Afficher les métadonnées détaillées de routage et de synchronisation des messages",
|
||||||
"contacts_clipboardEmpty": "Le presse-papiers est vide.",
|
"contacts_clipboardEmpty": "Le presse-papiers est vide.",
|
||||||
"contacts_contactImported": "Le contact a été importé.",
|
"contacts_contactImported": "Le contact a été importé.",
|
||||||
"contacts_floodAdvert": "Annonce à tout le réseau",
|
"contacts_floodAdvert": "Annonce à tout le réseau",
|
||||||
@@ -1592,9 +1604,260 @@
|
|||||||
"map_removeLast": "Supprimer le dernier",
|
"map_removeLast": "Supprimer le dernier",
|
||||||
"map_runTrace": "Exécuter la traçage de chemin",
|
"map_runTrace": "Exécuter la traçage de chemin",
|
||||||
"scanner_bluetoothOffMessage": "Veuillez activer le Bluetooth pour rechercher des appareils.",
|
"scanner_bluetoothOffMessage": "Veuillez activer le Bluetooth pour rechercher des appareils.",
|
||||||
|
"scanner_chromeRequired": "Navigateur Chrome requis",
|
||||||
|
"scanner_chromeRequiredMessage": "Cette application web nécessite Google Chrome ou un navigateur basé sur Chromium pour le support Bluetooth.",
|
||||||
"scanner_bluetoothOff": "Le Bluetooth est désactivé.",
|
"scanner_bluetoothOff": "Le Bluetooth est désactivé.",
|
||||||
"scanner_enableBluetooth": "Activer le Bluetooth",
|
"scanner_enableBluetooth": "Activer le Bluetooth",
|
||||||
|
"snrIndicator_lastSeen": "Dernière fois vu",
|
||||||
|
"snrIndicator_nearByRepeaters": "Répéteurs à proximité",
|
||||||
|
"chat_ShowAllPaths": "Afficher tous les chemins",
|
||||||
"settings_clientRepeatFreqWarning": "Pour les transmissions hors réseau, il est nécessaire d'utiliser les fréquences de 433, 869 ou 918 MHz.",
|
"settings_clientRepeatFreqWarning": "Pour les transmissions hors réseau, il est nécessaire d'utiliser les fréquences de 433, 869 ou 918 MHz.",
|
||||||
"settings_clientRepeatSubtitle": "Permettez à cet appareil de répéter les paquets de données pour les autres.",
|
"settings_clientRepeatSubtitle": "Permettez à cet appareil de répéter les paquets de données pour les autres.",
|
||||||
"settings_clientRepeat": "Répétition hors réseau"
|
"settings_clientRepeat": "Répétition hors réseau",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "Données d'élévation LOS : Open-Meteo (CC BY 4.0)",
|
||||||
|
"appSettings_unitsTitle": "Unités",
|
||||||
|
"appSettings_unitsMetric": "Métrique (m/km)",
|
||||||
|
"appSettings_unitsImperial": "Impérial (ft / mi)",
|
||||||
|
"map_lineOfSight": "Ligne de vue",
|
||||||
|
"map_losScreenTitle": "Ligne de vue",
|
||||||
|
"losSelectStartEnd": "Sélectionnez les nœuds de début et de fin pour LOS.",
|
||||||
|
"losRunFailed": "Échec de la vérification de la ligne de vue : {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Effacer tous les points",
|
||||||
|
"losRunToViewElevationProfile": "Exécutez LOS pour afficher le profil d'altitude",
|
||||||
|
"losMenuTitle": "Menu LOS",
|
||||||
|
"losMenuSubtitle": "Appuyez sur les nœuds ou appuyez longuement sur la carte pour des points personnalisés",
|
||||||
|
"losShowDisplayNodes": "Afficher les nœuds d'affichage",
|
||||||
|
"losCustomPoints": "Points personnalisés",
|
||||||
|
"losCustomPointLabel": "Personnalisé {index}",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Point A",
|
||||||
|
"losPointB": "Point B",
|
||||||
|
"losAntennaA": "Antenne A : {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Antenne B : {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Exécuter la LOS",
|
||||||
|
"losNoElevationData": "Aucune donnée d'altitude",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, LOS clair, clairance minimale {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, bloqué par {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "LOS : vérification...",
|
||||||
|
"losStatusNoData": "LOS : aucune donnée",
|
||||||
|
"losStatusSummary": "LOS : {clear}/{total} clair, {blocked} bloqué, {unknown} inconnu",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Données d'altitude indisponibles pour un ou plusieurs échantillons.",
|
||||||
|
"losErrorInvalidInput": "Données de points/d'altitude non valides pour le calcul de la LOS.",
|
||||||
|
"losRenameCustomPoint": "Renommer le point personnalisé",
|
||||||
|
"losPointName": "Nom du point",
|
||||||
|
"losShowPanelTooltip": "Afficher le panneau LOS",
|
||||||
|
"losHidePanelTooltip": "Masquer le panneau LOS",
|
||||||
|
"losElevationAttribution": "Données d’altitude : Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Horizon radio",
|
||||||
|
"losLegendLosBeam": "Ligne de visée",
|
||||||
|
"losLegendTerrain": "Terrain",
|
||||||
|
"losFrequencyLabel": "Fréquence",
|
||||||
|
"losFrequencyInfoTooltip": "Voir les détails du calcul",
|
||||||
|
"losFrequencyDialogTitle": "Calcul de l’horizon radio",
|
||||||
|
"losFrequencyDialogDescription": "À partir de k={baselineK} à {baselineFreq} MHz, le calcul ajuste le facteur k pour la bande actuelle de {frequencyMHz} MHz, ce qui définit la limite incurvée de l'horizon radio.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listFilter_addToFavorites": "Ajouter à mes favoris",
|
||||||
|
"listFilter_removeFromFavorites": "Supprimer des favoris",
|
||||||
|
"listFilter_favorites": "Préférences",
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_unread": "Non lu",
|
||||||
|
"contacts_searchFavorites": "Rechercher {number}{str} Favoris...",
|
||||||
|
"contacts_searchUsers": "Rechercher {number}{str} utilisateurs...",
|
||||||
|
"contacts_searchRoomServers": "Rechercher {number}{str} serveurs de salle...",
|
||||||
|
"contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...",
|
||||||
|
"contacts_searchContactsNoNumber": "Rechercher des contacts...",
|
||||||
|
"settings_contactSettings": "Paramètres de contact",
|
||||||
|
"settings_contactSettingsSubtitle": "Paramètres pour l'ajout de contacts",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Ajouter automatiquement les répéteurs",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Autoriser le compagnon à ajouter automatiquement les répéteurs découverts",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Ajouter automatiquement les serveurs de salle",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Autoriser le compagnon à ajouter automatiquement les serveurs de salles découverts",
|
||||||
|
"contactsSettings_otherTitle": "Autres paramètres liés aux contacts",
|
||||||
|
"contactsSettings_title": "Paramètres des contacts",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Ajouter automatiquement les utilisateurs",
|
||||||
|
"contactsSettings_autoAddTitle": "Découverte automatique",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Ajouter automatiquement les capteurs",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Autoriser le compagnon à ajouter automatiquement les utilisateurs découverts",
|
||||||
|
"discoveredContacts_noMatching": "Aucun contact correspondant",
|
||||||
|
"discoveredContacts_contactAdded": "Contact ajouté",
|
||||||
|
"discoveredContacts_addContact": "Ajouter un contact",
|
||||||
|
"discoveredContacts_copyContact": "Copier le contact dans le presse-papiers",
|
||||||
|
"discoveredContacts_deleteContact": "Supprimer le contact",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Écraser le plus ancien",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Autoriser le compagnon à ajouter automatiquement les capteurs découverts.",
|
||||||
|
"discoveredContacts_Title": "Contacts découverts",
|
||||||
|
"discoveredContacts_searchHint": "Rechercher des contacts découverts",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Lorsque la liste de contacts est pleine, le contact le plus ancien non favori sera remplacé.",
|
||||||
|
"common_deleteAll": "Supprimer tout",
|
||||||
|
"discoveredContacts_deleteContactAll": "Supprimer tous les contacts découverts",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Êtes-vous sûr de vouloir supprimer tous les contacts découverts ?",
|
||||||
|
"map_showGuessedLocations": "Afficher les emplacements des nœuds estimés",
|
||||||
|
"map_guessedLocation": "Lieu deviné",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenTitle": "Connectez via USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenSubtitle": "Sélectionnez un périphérique série détecté et connectez-vous directement à votre nœud MeshCore.",
|
||||||
|
"usbScreenStatus": "Sélectionnez un périphérique USB",
|
||||||
|
"usbScreenNote": "La communication série USB est active sur les appareils Android et les plateformes de bureau compatibles.",
|
||||||
|
"usbScreenEmptyState": "Aucun périphérique USB n'a été trouvé. Veuillez en brancher un et rafraîchir la page.",
|
||||||
|
"usbErrorPermissionDenied": "L'accès via USB a été refusé.",
|
||||||
|
"usbErrorDeviceMissing": "Le périphérique USB sélectionné n'est plus disponible.",
|
||||||
|
"usbErrorInvalidPort": "Sélectionnez un périphérique USB valide.",
|
||||||
|
"usbErrorBusy": "Une autre demande de connexion USB est déjà en cours.",
|
||||||
|
"usbErrorNotConnected": "Aucun appareil USB n'est connecté.",
|
||||||
|
"usbErrorOpenFailed": "Impossible d'ouvrir l'appareil USB sélectionné.",
|
||||||
|
"usbErrorConnectFailed": "Impossible de se connecter à l'appareil USB sélectionné.",
|
||||||
|
"usbErrorUnsupported": "La communication série USB n'est pas prise en charge sur cette plateforme.",
|
||||||
|
"usbErrorAlreadyActive": "Une connexion USB est déjà établie.",
|
||||||
|
"usbErrorNoDeviceSelected": "Aucun appareil USB n'a été sélectionné.",
|
||||||
|
"usbErrorPortClosed": "La connexion USB n'est pas établie.",
|
||||||
|
"usbFallbackDeviceName": "Dispositif de communication série sur le Web",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_notConnected": "Sélectionnez un périphérique USB",
|
||||||
|
"usbConnectionFailed": "Échec de la connexion USB : {error}",
|
||||||
|
"usbStatus_connecting": "Connexion au périphérique USB...",
|
||||||
|
"usbStatus_searching": "Recherche de périphériques USB...",
|
||||||
|
"usbErrorConnectTimedOut": "La connexion a expiré. Assurez-vous que l'appareil dispose du firmware USB Companion."
|
||||||
}
|
}
|
||||||
|
|||||||
+267
-4
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"channels_channelDeleteFailed": "Impossibile eliminare il canale \"{name}\"",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@@locale": "it",
|
"@@locale": "it",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Contatti",
|
"nav_contacts": "Contatti",
|
||||||
@@ -334,6 +342,8 @@
|
|||||||
"channels_publicChannel": "Canale pubblico",
|
"channels_publicChannel": "Canale pubblico",
|
||||||
"channels_privateChannel": "Canale privato",
|
"channels_privateChannel": "Canale privato",
|
||||||
"channels_editChannel": "Modifica canale",
|
"channels_editChannel": "Modifica canale",
|
||||||
|
"channels_muteChannel": "Silenzia canale",
|
||||||
|
"channels_unmuteChannel": "Attiva notifiche canale",
|
||||||
"channels_deleteChannel": "Elimina canale",
|
"channels_deleteChannel": "Elimina canale",
|
||||||
"channels_deleteChannelConfirm": "Eliminare \"{name}\"? Non può essere annullato.",
|
"channels_deleteChannelConfirm": "Eliminare \"{name}\"? Non può essere annullato.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
@@ -1351,12 +1361,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Vicini",
|
"repeater_neighbors": "Vicini",
|
||||||
"repeater_neighboursSubtitle": "Visualizza vicini di salto pari a zero.",
|
"repeater_neighborsSubtitle": "Visualizza vicini di salto pari a zero.",
|
||||||
"neighbors_receivedData": "Ricevute dati vicini",
|
"neighbors_receivedData": "Ricevute dati vicini",
|
||||||
"neighbors_requestTimedOut": "I vicini richiedono un timeout.",
|
"neighbors_requestTimedOut": "I vicini richiedono un timeout.",
|
||||||
"neighbors_errorLoading": "Errore nel caricamento dei vicini: {error}",
|
"neighbors_errorLoading": "Errore nel caricamento dei vicini: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Ripetitori Vicini",
|
"neighbors_repeatersNeighbors": "Ripetitori Vicini",
|
||||||
"neighbors_noData": "Nessun dato sugli vicini disponibile.",
|
"neighbors_noData": "Nessun dato sugli vicini disponibile.",
|
||||||
"channels_createPrivateChannel": "Crea un Canale Privato",
|
"channels_createPrivateChannel": "Crea un Canale Privato",
|
||||||
"channels_createPrivateChannelDesc": "Protetta con una chiave segreta.",
|
"channels_createPrivateChannelDesc": "Protetta con una chiave segreta.",
|
||||||
@@ -1551,6 +1561,8 @@
|
|||||||
"appSettings_languageRu": "Russo",
|
"appSettings_languageRu": "Russo",
|
||||||
"contacts_invalidAdvertFormat": "Dati di contatto non validi",
|
"contacts_invalidAdvertFormat": "Dati di contatto non validi",
|
||||||
"appSettings_languageUk": "Ucraino",
|
"appSettings_languageUk": "Ucraino",
|
||||||
|
"appSettings_enableMessageTracing": "Abilita tracciamento messaggi",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Mostra metadati dettagliati su instradamento e tempi per i messaggi",
|
||||||
"contacts_zeroHopAdvert": "Annuncio Zero Hop",
|
"contacts_zeroHopAdvert": "Annuncio Zero Hop",
|
||||||
"contacts_floodAdvert": "Annuncio alluvionale",
|
"contacts_floodAdvert": "Annuncio alluvionale",
|
||||||
"contacts_copyAdvertToClipboard": "Copia Annuncio negli Appunti",
|
"contacts_copyAdvertToClipboard": "Copia Annuncio negli Appunti",
|
||||||
@@ -1593,8 +1605,259 @@
|
|||||||
"map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.",
|
"map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.",
|
||||||
"scanner_bluetoothOff": "Il Bluetooth è disattivato.",
|
"scanner_bluetoothOff": "Il Bluetooth è disattivato.",
|
||||||
"scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.",
|
"scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.",
|
||||||
|
"scanner_chromeRequired": "Browser Chrome richiesto",
|
||||||
|
"scanner_chromeRequiredMessage": "Questa applicazione web richiede Google Chrome o un browser basato su Chromium per il supporto Bluetooth.",
|
||||||
"scanner_enableBluetooth": "Abilita il Bluetooth",
|
"scanner_enableBluetooth": "Abilita il Bluetooth",
|
||||||
|
"snrIndicator_nearByRepeaters": "Ripetitori vicini",
|
||||||
|
"snrIndicator_lastSeen": "Ultimo accesso",
|
||||||
|
"chat_ShowAllPaths": "Mostra tutti i percorsi",
|
||||||
"settings_clientRepeat": "Ripetizione \"fuori dalla rete\"",
|
"settings_clientRepeat": "Ripetizione \"fuori dalla rete\"",
|
||||||
"settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.",
|
"settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.",
|
||||||
"settings_clientRepeatSubtitle": "Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri."
|
"settings_clientRepeatSubtitle": "Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri.",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "Dati di elevazione LOS: Open-Meteo (CC BY 4.0)",
|
||||||
|
"appSettings_unitsTitle": "Unità",
|
||||||
|
"appSettings_unitsMetric": "Metrico (m/km)",
|
||||||
|
"appSettings_unitsImperial": "Imperiale (ft / mi)",
|
||||||
|
"map_lineOfSight": "Linea di vista",
|
||||||
|
"map_losScreenTitle": "Linea di vista",
|
||||||
|
"losSelectStartEnd": "Seleziona i nodi iniziali e finali per la LOS.",
|
||||||
|
"losRunFailed": "Controllo della linea di vista fallito: {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Cancella tutti i punti",
|
||||||
|
"losRunToViewElevationProfile": "Eseguire LOS per visualizzare il profilo altimetrico",
|
||||||
|
"losMenuTitle": "Menù LOS",
|
||||||
|
"losMenuSubtitle": "Tocca i nodi o premi a lungo la mappa per punti personalizzati",
|
||||||
|
"losShowDisplayNodes": "Mostra i nodi di visualizzazione",
|
||||||
|
"losCustomPoints": "Punti personalizzati",
|
||||||
|
"losCustomPointLabel": "Personalizzato {index}",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Punto A",
|
||||||
|
"losPointB": "Punto B",
|
||||||
|
"losAntennaA": "Antenna A: {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Antenna B: {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Esegui LOS",
|
||||||
|
"losNoElevationData": "Nessun dato di elevazione",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, libera LOS, distanza minima {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, bloccato da {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "LOS: controllo...",
|
||||||
|
"losStatusNoData": "LOS: nessun dato",
|
||||||
|
"losStatusSummary": "LOS: {clear}/{total} libera, {blocked} bloccato, {unknown} sconosciuto",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Dati di elevazione non disponibili per uno o più campioni.",
|
||||||
|
"losErrorInvalidInput": "Dati punti/elevazione non validi per il calcolo della LOS.",
|
||||||
|
"losRenameCustomPoint": "Rinomina punto personalizzato",
|
||||||
|
"losPointName": "Nome del punto",
|
||||||
|
"losShowPanelTooltip": "Mostra il pannello LOS",
|
||||||
|
"losHidePanelTooltip": "Nascondi il pannello LOS",
|
||||||
|
"losElevationAttribution": "Dati di elevazione: Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Orizzonte radio",
|
||||||
|
"losLegendLosBeam": "Linea di vista",
|
||||||
|
"losLegendTerrain": "Terreno",
|
||||||
|
"losFrequencyLabel": "Frequenza",
|
||||||
|
"losFrequencyInfoTooltip": "Visualizza i dettagli del calcolo",
|
||||||
|
"losFrequencyDialogTitle": "Calcolo 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.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listFilter_addToFavorites": "Aggiungi ai preferiti",
|
||||||
|
"listFilter_removeFromFavorites": "Rimuovi dai preferiti",
|
||||||
|
"listFilter_favorites": "Preferiti",
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_searchUsers": "Cerca {number}{str} Utenti...",
|
||||||
|
"contacts_searchContactsNoNumber": "Cerca Contatti...",
|
||||||
|
"contacts_searchFavorites": "Cerca {number}{str} Preferiti...",
|
||||||
|
"contacts_unread": "Non letti",
|
||||||
|
"contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...",
|
||||||
|
"contacts_searchRoomServers": "Cerca {number}{str} server Room...",
|
||||||
|
"contactsSettings_title": "Impostazioni dei contatti",
|
||||||
|
"settings_contactSettings": "Impostazioni di contatto",
|
||||||
|
"contactsSettings_otherTitle": "Altre impostazioni relative ai contatti",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Consenti al compagno di aggiungere automaticamente gli utenti scoperti.",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Aggiungere ripetitori automaticamente",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Consenti al compagno di aggiungere automaticamente i server delle stanze scoperte.",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Aggiungere automaticamente i sensori",
|
||||||
|
"settings_contactSettingsSubtitle": "Impostazioni per l'aggiunta dei contatti",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Aggiungere utenti automaticamente",
|
||||||
|
"contactsSettings_autoAddTitle": "Scoperta automatica",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Consenti al compagno di aggiungere automaticamente i sensori scoperti",
|
||||||
|
"discoveredContacts_noMatching": "Nessun contatto corrispondente",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Consenti al compagno di aggiungere automaticamente i ripetitori scoperti.",
|
||||||
|
"discoveredContacts_searchHint": "Cerca contatti scoperti",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Aggiungere automaticamente i server delle stanze",
|
||||||
|
"discoveredContacts_addContact": "Aggiungi contatto",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Sostituisci il più vecchio",
|
||||||
|
"discoveredContacts_Title": "Contatti scoperti",
|
||||||
|
"discoveredContacts_contactAdded": "Contatto aggiunto",
|
||||||
|
"discoveredContacts_deleteContact": "Elimina Contatto",
|
||||||
|
"discoveredContacts_copyContact": "Copia contatto negli appunti",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Quando l'elenco dei contatti è pieno, il contatto più vecchio non tra i preferiti verrà sostituito.",
|
||||||
|
"common_deleteAll": "Elimina tutto",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Sei sicuro di voler eliminare tutti i contatti scoperti?",
|
||||||
|
"discoveredContacts_deleteContactAll": "Eliminare tutti i contatti scoperti",
|
||||||
|
"map_guessedLocation": "Località indovinata",
|
||||||
|
"map_showGuessedLocations": "Mostra le posizioni stimate dei nodi",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenSubtitle": "Seleziona il dispositivo seriale rilevato e connettilo direttamente al tuo nodo MeshCore.",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenTitle": "Connessione tramite USB",
|
||||||
|
"usbScreenStatus": "Seleziona un dispositivo USB",
|
||||||
|
"usbScreenNote": "La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.",
|
||||||
|
"usbScreenEmptyState": "Nessun dispositivo USB rilevato. Collegare uno e aggiornare.",
|
||||||
|
"usbErrorPermissionDenied": "È stato negato l'accesso tramite USB.",
|
||||||
|
"usbErrorDeviceMissing": "Il dispositivo USB selezionato non è più disponibile.",
|
||||||
|
"usbErrorInvalidPort": "Seleziona un dispositivo USB valido.",
|
||||||
|
"usbErrorBusy": "Un'altra richiesta di connessione tramite USB è già in corso.",
|
||||||
|
"usbErrorNotConnected": "Non è collegato alcun dispositivo USB.",
|
||||||
|
"usbErrorOpenFailed": "Impossibile aprire il dispositivo USB selezionato.",
|
||||||
|
"usbErrorConnectFailed": "Impossibile connettersi al dispositivo USB selezionato.",
|
||||||
|
"usbErrorUnsupported": "La comunicazione seriale tramite USB non è supportata su questa piattaforma.",
|
||||||
|
"usbErrorAlreadyActive": "La connessione USB è già attiva.",
|
||||||
|
"usbErrorNoDeviceSelected": "Non è stato selezionato alcun dispositivo USB.",
|
||||||
|
"usbErrorPortClosed": "La connessione USB non è attiva.",
|
||||||
|
"usbFallbackDeviceName": "Dispositivo per comunicazione seriale su rete",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_searching": "Ricerca di dispositivi USB...",
|
||||||
|
"usbConnectionFailed": "Errore nella connessione USB: {error}",
|
||||||
|
"usbStatus_notConnected": "Seleziona un dispositivo USB",
|
||||||
|
"usbStatus_connecting": "Connessione al dispositivo USB...",
|
||||||
|
"usbErrorConnectTimedOut": "La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion."
|
||||||
}
|
}
|
||||||
|
|||||||
+693
-18
@@ -184,6 +184,12 @@ abstract class AppLocalizations {
|
|||||||
/// **'Delete'**
|
/// **'Delete'**
|
||||||
String get common_delete;
|
String get common_delete;
|
||||||
|
|
||||||
|
/// No description provided for @common_deleteAll.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Delete All'**
|
||||||
|
String get common_deleteAll;
|
||||||
|
|
||||||
/// No description provided for @common_close.
|
/// No description provided for @common_close.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -316,6 +322,150 @@ abstract class AppLocalizations {
|
|||||||
/// **'MeshCore Open'**
|
/// **'MeshCore Open'**
|
||||||
String get scanner_title;
|
String get scanner_title;
|
||||||
|
|
||||||
|
/// No description provided for @connectionChoiceUsbLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'USB'**
|
||||||
|
String get connectionChoiceUsbLabel;
|
||||||
|
|
||||||
|
/// No description provided for @connectionChoiceBluetoothLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Bluetooth'**
|
||||||
|
String get connectionChoiceBluetoothLabel;
|
||||||
|
|
||||||
|
/// No description provided for @usbScreenTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Connect over USB'**
|
||||||
|
String get usbScreenTitle;
|
||||||
|
|
||||||
|
/// No description provided for @usbScreenSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Choose a detected serial device and connect directly to your MeshCore node.'**
|
||||||
|
String get usbScreenSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @usbScreenStatus.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Select a USB device'**
|
||||||
|
String get usbScreenStatus;
|
||||||
|
|
||||||
|
/// No description provided for @usbScreenNote.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'USB serial is active on supported Android devices and desktop platforms.'**
|
||||||
|
String get usbScreenNote;
|
||||||
|
|
||||||
|
/// No description provided for @usbScreenEmptyState.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No USB devices found. Plug one in and refresh.'**
|
||||||
|
String get usbScreenEmptyState;
|
||||||
|
|
||||||
|
/// No description provided for @usbErrorPermissionDenied.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'USB permission was denied.'**
|
||||||
|
String get usbErrorPermissionDenied;
|
||||||
|
|
||||||
|
/// No description provided for @usbErrorDeviceMissing.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'The selected USB device is no longer available.'**
|
||||||
|
String get usbErrorDeviceMissing;
|
||||||
|
|
||||||
|
/// No description provided for @usbErrorInvalidPort.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Select a valid USB device.'**
|
||||||
|
String get usbErrorInvalidPort;
|
||||||
|
|
||||||
|
/// No description provided for @usbErrorBusy.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Another USB connection request is already in progress.'**
|
||||||
|
String get usbErrorBusy;
|
||||||
|
|
||||||
|
/// No description provided for @usbErrorNotConnected.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No USB device is connected.'**
|
||||||
|
String get usbErrorNotConnected;
|
||||||
|
|
||||||
|
/// No description provided for @usbErrorOpenFailed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Failed to open the selected USB device.'**
|
||||||
|
String get usbErrorOpenFailed;
|
||||||
|
|
||||||
|
/// No description provided for @usbErrorConnectFailed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Failed to connect to the selected USB device.'**
|
||||||
|
String get usbErrorConnectFailed;
|
||||||
|
|
||||||
|
/// No description provided for @usbErrorUnsupported.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'USB serial is not supported on this platform.'**
|
||||||
|
String get usbErrorUnsupported;
|
||||||
|
|
||||||
|
/// No description provided for @usbErrorAlreadyActive.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'A USB connection is already active.'**
|
||||||
|
String get usbErrorAlreadyActive;
|
||||||
|
|
||||||
|
/// No description provided for @usbErrorNoDeviceSelected.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No USB device was selected.'**
|
||||||
|
String get usbErrorNoDeviceSelected;
|
||||||
|
|
||||||
|
/// No description provided for @usbErrorPortClosed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'The USB connection is not open.'**
|
||||||
|
String get usbErrorPortClosed;
|
||||||
|
|
||||||
|
/// No description provided for @usbErrorConnectTimedOut.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Connection timed out. Make sure the device has USB Companion firmware.'**
|
||||||
|
String get usbErrorConnectTimedOut;
|
||||||
|
|
||||||
|
/// No description provided for @usbFallbackDeviceName.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Web Serial Device'**
|
||||||
|
String get usbFallbackDeviceName;
|
||||||
|
|
||||||
|
/// No description provided for @usbStatus_notConnected.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Select a USB device'**
|
||||||
|
String get usbStatus_notConnected;
|
||||||
|
|
||||||
|
/// No description provided for @usbStatus_connecting.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Connecting to USB device...'**
|
||||||
|
String get usbStatus_connecting;
|
||||||
|
|
||||||
|
/// No description provided for @usbStatus_searching.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Searching for USB devices...'**
|
||||||
|
String get usbStatus_searching;
|
||||||
|
|
||||||
|
/// No description provided for @usbConnectionFailed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'USB connection failed: {error}'**
|
||||||
|
String usbConnectionFailed(String error);
|
||||||
|
|
||||||
/// No description provided for @scanner_scanning.
|
/// No description provided for @scanner_scanning.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -388,6 +538,18 @@ abstract class AppLocalizations {
|
|||||||
/// **'Please turn on Bluetooth to scan for devices'**
|
/// **'Please turn on Bluetooth to scan for devices'**
|
||||||
String get scanner_bluetoothOffMessage;
|
String get scanner_bluetoothOffMessage;
|
||||||
|
|
||||||
|
/// No description provided for @scanner_chromeRequired.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Chrome Browser Required'**
|
||||||
|
String get scanner_chromeRequired;
|
||||||
|
|
||||||
|
/// No description provided for @scanner_chromeRequiredMessage.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'This web application requires Google Chrome or a Chromium-based browser for Bluetooth support.'**
|
||||||
|
String get scanner_chromeRequiredMessage;
|
||||||
|
|
||||||
/// No description provided for @scanner_enableBluetooth.
|
/// No description provided for @scanner_enableBluetooth.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -544,6 +706,18 @@ abstract class AppLocalizations {
|
|||||||
/// **'Longitude'**
|
/// **'Longitude'**
|
||||||
String get settings_longitude;
|
String get settings_longitude;
|
||||||
|
|
||||||
|
/// No description provided for @settings_contactSettings.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Contact Settings'**
|
||||||
|
String get settings_contactSettings;
|
||||||
|
|
||||||
|
/// No description provided for @settings_contactSettingsSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Settings for how contacts are added.'**
|
||||||
|
String get settings_contactSettingsSubtitle;
|
||||||
|
|
||||||
/// No description provided for @settings_privacyMode.
|
/// No description provided for @settings_privacyMode.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -700,6 +874,12 @@ abstract class AppLocalizations {
|
|||||||
/// **'An open-source Flutter client for MeshCore LoRa mesh networking devices.'**
|
/// **'An open-source Flutter client for MeshCore LoRa mesh networking devices.'**
|
||||||
String get settings_aboutDescription;
|
String get settings_aboutDescription;
|
||||||
|
|
||||||
|
/// No description provided for @settings_aboutOpenMeteoAttribution.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'LOS elevation data: Open-Meteo (CC BY 4.0)'**
|
||||||
|
String get settings_aboutOpenMeteoAttribution;
|
||||||
|
|
||||||
/// No description provided for @settings_infoName.
|
/// No description provided for @settings_infoName.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -964,6 +1144,18 @@ abstract class AppLocalizations {
|
|||||||
/// **'Українська'**
|
/// **'Українська'**
|
||||||
String get appSettings_languageUk;
|
String get appSettings_languageUk;
|
||||||
|
|
||||||
|
/// No description provided for @appSettings_enableMessageTracing.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Enable Message Tracing'**
|
||||||
|
String get appSettings_enableMessageTracing;
|
||||||
|
|
||||||
|
/// No description provided for @appSettings_enableMessageTracingSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Show detailed routing and timing metadata for messages'**
|
||||||
|
String get appSettings_enableMessageTracingSubtitle;
|
||||||
|
|
||||||
/// No description provided for @appSettings_notifications.
|
/// No description provided for @appSettings_notifications.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -1240,6 +1432,24 @@ abstract class AppLocalizations {
|
|||||||
/// **'Offline Map Cache'**
|
/// **'Offline Map Cache'**
|
||||||
String get appSettings_offlineMapCache;
|
String get appSettings_offlineMapCache;
|
||||||
|
|
||||||
|
/// No description provided for @appSettings_unitsTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Units'**
|
||||||
|
String get appSettings_unitsTitle;
|
||||||
|
|
||||||
|
/// No description provided for @appSettings_unitsMetric.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Metric (m / km)'**
|
||||||
|
String get appSettings_unitsMetric;
|
||||||
|
|
||||||
|
/// No description provided for @appSettings_unitsImperial.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Imperial (ft / mi)'**
|
||||||
|
String get appSettings_unitsImperial;
|
||||||
|
|
||||||
/// No description provided for @appSettings_noAreaSelected.
|
/// No description provided for @appSettings_noAreaSelected.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -1300,11 +1510,47 @@ abstract class AppLocalizations {
|
|||||||
/// **'Contacts will appear when devices advertise'**
|
/// **'Contacts will appear when devices advertise'**
|
||||||
String get contacts_contactsWillAppear;
|
String get contacts_contactsWillAppear;
|
||||||
|
|
||||||
|
/// No description provided for @contacts_unread.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Unread'**
|
||||||
|
String get contacts_unread;
|
||||||
|
|
||||||
|
/// No description provided for @contacts_searchContactsNoNumber.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Search Contacts...'**
|
||||||
|
String get contacts_searchContactsNoNumber;
|
||||||
|
|
||||||
/// No description provided for @contacts_searchContacts.
|
/// No description provided for @contacts_searchContacts.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Search contacts...'**
|
/// **'Search {number}{str} Contacts...'**
|
||||||
String get contacts_searchContacts;
|
String contacts_searchContacts(int number, String str);
|
||||||
|
|
||||||
|
/// No description provided for @contacts_searchFavorites.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Search {number}{str} Favorites...'**
|
||||||
|
String contacts_searchFavorites(int number, String str);
|
||||||
|
|
||||||
|
/// No description provided for @contacts_searchUsers.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Search {number}{str} Users...'**
|
||||||
|
String contacts_searchUsers(int number, String str);
|
||||||
|
|
||||||
|
/// No description provided for @contacts_searchRepeaters.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Search {number}{str} Repeaters...'**
|
||||||
|
String contacts_searchRepeaters(int number, String str);
|
||||||
|
|
||||||
|
/// No description provided for @contacts_searchRoomServers.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Search {number}{str} Room servers...'**
|
||||||
|
String contacts_searchRoomServers(int number, String str);
|
||||||
|
|
||||||
/// No description provided for @contacts_noUnreadContacts.
|
/// No description provided for @contacts_noUnreadContacts.
|
||||||
///
|
///
|
||||||
@@ -1417,37 +1663,37 @@ abstract class AppLocalizations {
|
|||||||
/// No description provided for @contacts_lastSeenNow.
|
/// No description provided for @contacts_lastSeenNow.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Last seen now'**
|
/// **'recently'**
|
||||||
String get contacts_lastSeenNow;
|
String get contacts_lastSeenNow;
|
||||||
|
|
||||||
/// No description provided for @contacts_lastSeenMinsAgo.
|
/// No description provided for @contacts_lastSeenMinsAgo.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Last seen {minutes} mins ago'**
|
/// **'~ {minutes} min.'**
|
||||||
String contacts_lastSeenMinsAgo(int minutes);
|
String contacts_lastSeenMinsAgo(int minutes);
|
||||||
|
|
||||||
/// No description provided for @contacts_lastSeenHourAgo.
|
/// No description provided for @contacts_lastSeenHourAgo.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Last seen 1 hour ago'**
|
/// **'~ 1 hour'**
|
||||||
String get contacts_lastSeenHourAgo;
|
String get contacts_lastSeenHourAgo;
|
||||||
|
|
||||||
/// No description provided for @contacts_lastSeenHoursAgo.
|
/// No description provided for @contacts_lastSeenHoursAgo.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Last seen {hours} hours ago'**
|
/// **'~ {hours} hours'**
|
||||||
String contacts_lastSeenHoursAgo(int hours);
|
String contacts_lastSeenHoursAgo(int hours);
|
||||||
|
|
||||||
/// No description provided for @contacts_lastSeenDayAgo.
|
/// No description provided for @contacts_lastSeenDayAgo.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Last seen 1 day ago'**
|
/// **'~ 1 day'**
|
||||||
String get contacts_lastSeenDayAgo;
|
String get contacts_lastSeenDayAgo;
|
||||||
|
|
||||||
/// No description provided for @contacts_lastSeenDaysAgo.
|
/// No description provided for @contacts_lastSeenDaysAgo.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Last seen {days} days ago'**
|
/// **'~ {days} days'**
|
||||||
String contacts_lastSeenDaysAgo(int days);
|
String contacts_lastSeenDaysAgo(int days);
|
||||||
|
|
||||||
/// No description provided for @channels_title.
|
/// No description provided for @channels_title.
|
||||||
@@ -1522,6 +1768,18 @@ abstract class AppLocalizations {
|
|||||||
/// **'Edit channel'**
|
/// **'Edit channel'**
|
||||||
String get channels_editChannel;
|
String get channels_editChannel;
|
||||||
|
|
||||||
|
/// No description provided for @channels_muteChannel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Mute channel'**
|
||||||
|
String get channels_muteChannel;
|
||||||
|
|
||||||
|
/// No description provided for @channels_unmuteChannel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Unmute channel'**
|
||||||
|
String get channels_unmuteChannel;
|
||||||
|
|
||||||
/// No description provided for @channels_deleteChannel.
|
/// No description provided for @channels_deleteChannel.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -1534,6 +1792,12 @@ abstract class AppLocalizations {
|
|||||||
/// **'Delete \"{name}\"? This cannot be undone.'**
|
/// **'Delete \"{name}\"? This cannot be undone.'**
|
||||||
String channels_deleteChannelConfirm(String name);
|
String channels_deleteChannelConfirm(String name);
|
||||||
|
|
||||||
|
/// No description provided for @channels_channelDeleteFailed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Failed to delete channel \"{name}\"'**
|
||||||
|
String channels_channelDeleteFailed(String name);
|
||||||
|
|
||||||
/// No description provided for @channels_channelDeleted.
|
/// No description provided for @channels_channelDeleted.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -2032,6 +2296,12 @@ abstract class AppLocalizations {
|
|||||||
/// **'Path Management'**
|
/// **'Path Management'**
|
||||||
String get chat_pathManagement;
|
String get chat_pathManagement;
|
||||||
|
|
||||||
|
/// No description provided for @chat_ShowAllPaths.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Show all paths'**
|
||||||
|
String get chat_ShowAllPaths;
|
||||||
|
|
||||||
/// No description provided for @chat_routingMode.
|
/// No description provided for @chat_routingMode.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -2284,6 +2554,18 @@ abstract class AppLocalizations {
|
|||||||
/// **'Node Map'**
|
/// **'Node Map'**
|
||||||
String get map_title;
|
String get map_title;
|
||||||
|
|
||||||
|
/// No description provided for @map_lineOfSight.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Line of Sight'**
|
||||||
|
String get map_lineOfSight;
|
||||||
|
|
||||||
|
/// No description provided for @map_losScreenTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Line of Sight'**
|
||||||
|
String get map_losScreenTitle;
|
||||||
|
|
||||||
/// No description provided for @map_noNodesWithLocation.
|
/// No description provided for @map_noNodesWithLocation.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -2500,6 +2782,18 @@ abstract class AppLocalizations {
|
|||||||
/// **'Show shared markers'**
|
/// **'Show shared markers'**
|
||||||
String get map_showSharedMarkers;
|
String get map_showSharedMarkers;
|
||||||
|
|
||||||
|
/// No description provided for @map_showGuessedLocations.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Show guessed node locations'**
|
||||||
|
String get map_showGuessedLocations;
|
||||||
|
|
||||||
|
/// No description provided for @map_guessedLocation.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Guessed location'**
|
||||||
|
String get map_guessedLocation;
|
||||||
|
|
||||||
/// No description provided for @map_lastSeenTime.
|
/// No description provided for @map_lastSeenTime.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -3027,17 +3321,17 @@ abstract class AppLocalizations {
|
|||||||
/// **'Send commands to the repeater'**
|
/// **'Send commands to the repeater'**
|
||||||
String get repeater_cliSubtitle;
|
String get repeater_cliSubtitle;
|
||||||
|
|
||||||
/// No description provided for @repeater_neighbours.
|
/// No description provided for @repeater_neighbors.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Neighbors'**
|
/// **'Neighbors'**
|
||||||
String get repeater_neighbours;
|
String get repeater_neighbors;
|
||||||
|
|
||||||
/// No description provided for @repeater_neighboursSubtitle.
|
/// No description provided for @repeater_neighborsSubtitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'View zero hop neighbors.'**
|
/// **'View zero hop neighbors.'**
|
||||||
String get repeater_neighboursSubtitle;
|
String get repeater_neighborsSubtitle;
|
||||||
|
|
||||||
/// No description provided for @repeater_settings.
|
/// No description provided for @repeater_settings.
|
||||||
///
|
///
|
||||||
@@ -4181,13 +4475,13 @@ abstract class AppLocalizations {
|
|||||||
/// No description provided for @neighbors_receivedData.
|
/// No description provided for @neighbors_receivedData.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Received Neighbours Data'**
|
/// **'Received Neighbors Data'**
|
||||||
String get neighbors_receivedData;
|
String get neighbors_receivedData;
|
||||||
|
|
||||||
/// No description provided for @neighbors_requestTimedOut.
|
/// No description provided for @neighbors_requestTimedOut.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Neighbours request timed out.'**
|
/// **'Neighbors request timed out.'**
|
||||||
String get neighbors_requestTimedOut;
|
String get neighbors_requestTimedOut;
|
||||||
|
|
||||||
/// No description provided for @neighbors_errorLoading.
|
/// No description provided for @neighbors_errorLoading.
|
||||||
@@ -4196,16 +4490,16 @@ abstract class AppLocalizations {
|
|||||||
/// **'Error loading neighbors: {error}'**
|
/// **'Error loading neighbors: {error}'**
|
||||||
String neighbors_errorLoading(String error);
|
String neighbors_errorLoading(String error);
|
||||||
|
|
||||||
/// No description provided for @neighbors_repeatersNeighbours.
|
/// No description provided for @neighbors_repeatersNeighbors.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Repeaters Neighbours'**
|
/// **'Repeaters Neighbors'**
|
||||||
String get neighbors_repeatersNeighbours;
|
String get neighbors_repeatersNeighbors;
|
||||||
|
|
||||||
/// No description provided for @neighbors_noData.
|
/// No description provided for @neighbors_noData.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'No neighbours data available.'**
|
/// **'No neighbors data available.'**
|
||||||
String get neighbors_noData;
|
String get neighbors_noData;
|
||||||
|
|
||||||
/// No description provided for @neighbors_unknownContact.
|
/// No description provided for @neighbors_unknownContact.
|
||||||
@@ -4700,6 +4994,24 @@ abstract class AppLocalizations {
|
|||||||
/// **'All'**
|
/// **'All'**
|
||||||
String get listFilter_all;
|
String get listFilter_all;
|
||||||
|
|
||||||
|
/// No description provided for @listFilter_favorites.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Favorites'**
|
||||||
|
String get listFilter_favorites;
|
||||||
|
|
||||||
|
/// No description provided for @listFilter_addToFavorites.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Add to favorites'**
|
||||||
|
String get listFilter_addToFavorites;
|
||||||
|
|
||||||
|
/// No description provided for @listFilter_removeFromFavorites.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Remove from favorites'**
|
||||||
|
String get listFilter_removeFromFavorites;
|
||||||
|
|
||||||
/// No description provided for @listFilter_users.
|
/// No description provided for @listFilter_users.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -4766,6 +5078,225 @@ abstract class AppLocalizations {
|
|||||||
/// **'Clear path.'**
|
/// **'Clear path.'**
|
||||||
String get pathTrace_clearTooltip;
|
String get pathTrace_clearTooltip;
|
||||||
|
|
||||||
|
/// No description provided for @losSelectStartEnd.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Select start and end nodes for LOS.'**
|
||||||
|
String get losSelectStartEnd;
|
||||||
|
|
||||||
|
/// No description provided for @losRunFailed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Line-of-sight check failed: {error}'**
|
||||||
|
String losRunFailed(String error);
|
||||||
|
|
||||||
|
/// No description provided for @losClearAllPoints.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Clear all points'**
|
||||||
|
String get losClearAllPoints;
|
||||||
|
|
||||||
|
/// No description provided for @losRunToViewElevationProfile.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Run LOS to view elevation profile'**
|
||||||
|
String get losRunToViewElevationProfile;
|
||||||
|
|
||||||
|
/// No description provided for @losMenuTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'LOS Menu'**
|
||||||
|
String get losMenuTitle;
|
||||||
|
|
||||||
|
/// No description provided for @losMenuSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Tap nodes or long-press map for custom points'**
|
||||||
|
String get losMenuSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @losShowDisplayNodes.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Show display nodes'**
|
||||||
|
String get losShowDisplayNodes;
|
||||||
|
|
||||||
|
/// No description provided for @losCustomPoints.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Custom points'**
|
||||||
|
String get losCustomPoints;
|
||||||
|
|
||||||
|
/// No description provided for @losCustomPointLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Custom {index}'**
|
||||||
|
String losCustomPointLabel(int index);
|
||||||
|
|
||||||
|
/// No description provided for @losPointA.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Point A'**
|
||||||
|
String get losPointA;
|
||||||
|
|
||||||
|
/// No description provided for @losPointB.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Point B'**
|
||||||
|
String get losPointB;
|
||||||
|
|
||||||
|
/// No description provided for @losAntennaA.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Antenna A: {value} {unit}'**
|
||||||
|
String losAntennaA(String value, String unit);
|
||||||
|
|
||||||
|
/// No description provided for @losAntennaB.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Antenna B: {value} {unit}'**
|
||||||
|
String losAntennaB(String value, String unit);
|
||||||
|
|
||||||
|
/// No description provided for @losRun.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Run LOS'**
|
||||||
|
String get losRun;
|
||||||
|
|
||||||
|
/// No description provided for @losNoElevationData.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No elevation data'**
|
||||||
|
String get losNoElevationData;
|
||||||
|
|
||||||
|
/// No description provided for @losProfileClear.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{distance} {distanceUnit}, clear LOS, min clearance {clearance} {heightUnit}'**
|
||||||
|
String losProfileClear(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String clearance,
|
||||||
|
String heightUnit,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// No description provided for @losProfileBlocked.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{distance} {distanceUnit}, blocked by {obstruction} {heightUnit}'**
|
||||||
|
String losProfileBlocked(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// No description provided for @losStatusChecking.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'LOS: checking...'**
|
||||||
|
String get losStatusChecking;
|
||||||
|
|
||||||
|
/// No description provided for @losStatusNoData.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'LOS: no data'**
|
||||||
|
String get losStatusNoData;
|
||||||
|
|
||||||
|
/// No description provided for @losStatusSummary.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'LOS: {clear}/{total} clear, {blocked} blocked, {unknown} unknown'**
|
||||||
|
String losStatusSummary(int clear, int total, int blocked, int unknown);
|
||||||
|
|
||||||
|
/// No description provided for @losErrorElevationUnavailable.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Elevation data unavailable for one or more samples.'**
|
||||||
|
String get losErrorElevationUnavailable;
|
||||||
|
|
||||||
|
/// No description provided for @losErrorInvalidInput.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Invalid points/elevation data for LOS calculation.'**
|
||||||
|
String get losErrorInvalidInput;
|
||||||
|
|
||||||
|
/// No description provided for @losRenameCustomPoint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Rename custom point'**
|
||||||
|
String get losRenameCustomPoint;
|
||||||
|
|
||||||
|
/// No description provided for @losPointName.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Point name'**
|
||||||
|
String get losPointName;
|
||||||
|
|
||||||
|
/// No description provided for @losShowPanelTooltip.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Show LOS panel'**
|
||||||
|
String get losShowPanelTooltip;
|
||||||
|
|
||||||
|
/// No description provided for @losHidePanelTooltip.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Hide LOS panel'**
|
||||||
|
String get losHidePanelTooltip;
|
||||||
|
|
||||||
|
/// No description provided for @losElevationAttribution.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Elevation data: Open-Meteo (CC BY 4.0)'**
|
||||||
|
String get losElevationAttribution;
|
||||||
|
|
||||||
|
/// No description provided for @losLegendRadioHorizon.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Radio horizon'**
|
||||||
|
String get losLegendRadioHorizon;
|
||||||
|
|
||||||
|
/// No description provided for @losLegendLosBeam.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'LOS beam'**
|
||||||
|
String get losLegendLosBeam;
|
||||||
|
|
||||||
|
/// No description provided for @losLegendTerrain.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Terrain'**
|
||||||
|
String get losLegendTerrain;
|
||||||
|
|
||||||
|
/// No description provided for @losFrequencyLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Frequency'**
|
||||||
|
String get losFrequencyLabel;
|
||||||
|
|
||||||
|
/// No description provided for @losFrequencyInfoTooltip.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'View calculation details'**
|
||||||
|
String get losFrequencyInfoTooltip;
|
||||||
|
|
||||||
|
/// No description provided for @losFrequencyDialogTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Radio horizon calculation'**
|
||||||
|
String get losFrequencyDialogTitle;
|
||||||
|
|
||||||
|
/// Explain how the calculation uses the baseline frequency and derived k-factor.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Starting from k={baselineK} at {baselineFreq} MHz, the calculation adjusts the k-factor for the current {frequencyMHz} MHz band, which defines the curved radio horizon cap.'**
|
||||||
|
String losFrequencyDialogDescription(
|
||||||
|
double baselineK,
|
||||||
|
double baselineFreq,
|
||||||
|
double frequencyMHz,
|
||||||
|
double kFactor,
|
||||||
|
);
|
||||||
|
|
||||||
/// No description provided for @contacts_pathTrace.
|
/// No description provided for @contacts_pathTrace.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -5023,6 +5554,150 @@ abstract class AppLocalizations {
|
|||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'meshcore-open GPX map data export'**
|
/// **'meshcore-open GPX map data export'**
|
||||||
String get settings_gpxExportShareSubject;
|
String get settings_gpxExportShareSubject;
|
||||||
|
|
||||||
|
/// No description provided for @snrIndicator_nearByRepeaters.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Nearby Repeaters'**
|
||||||
|
String get snrIndicator_nearByRepeaters;
|
||||||
|
|
||||||
|
/// No description provided for @snrIndicator_lastSeen.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Last seen'**
|
||||||
|
String get snrIndicator_lastSeen;
|
||||||
|
|
||||||
|
/// No description provided for @contactsSettings_title.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Contacts settings'**
|
||||||
|
String get contactsSettings_title;
|
||||||
|
|
||||||
|
/// No description provided for @contactsSettings_autoAddTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Automatic Discovery'**
|
||||||
|
String get contactsSettings_autoAddTitle;
|
||||||
|
|
||||||
|
/// No description provided for @contactsSettings_otherTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Other contact related settings'**
|
||||||
|
String get contactsSettings_otherTitle;
|
||||||
|
|
||||||
|
/// No description provided for @contactsSettings_autoAddUsersTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Auto-add users'**
|
||||||
|
String get contactsSettings_autoAddUsersTitle;
|
||||||
|
|
||||||
|
/// No description provided for @contactsSettings_autoAddUsersSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Allow the companion to automatically add discovered users.'**
|
||||||
|
String get contactsSettings_autoAddUsersSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @contactsSettings_autoAddRepeatersTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Auto-add repeaters'**
|
||||||
|
String get contactsSettings_autoAddRepeatersTitle;
|
||||||
|
|
||||||
|
/// No description provided for @contactsSettings_autoAddRepeatersSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Allow the companion to automatically add discovered repeaters.'**
|
||||||
|
String get contactsSettings_autoAddRepeatersSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @contactsSettings_autoAddRoomServersTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Auto-add room servers'**
|
||||||
|
String get contactsSettings_autoAddRoomServersTitle;
|
||||||
|
|
||||||
|
/// No description provided for @contactsSettings_autoAddRoomServersSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Allow the companion to automatically add discovered room servers.'**
|
||||||
|
String get contactsSettings_autoAddRoomServersSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @contactsSettings_autoAddSensorsTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Auto-add sensors'**
|
||||||
|
String get contactsSettings_autoAddSensorsTitle;
|
||||||
|
|
||||||
|
/// No description provided for @contactsSettings_autoAddSensorsSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Allow the companion to automatically add discovered sensors.'**
|
||||||
|
String get contactsSettings_autoAddSensorsSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @contactsSettings_overwriteOldestTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Overwrite Oldest'**
|
||||||
|
String get contactsSettings_overwriteOldestTitle;
|
||||||
|
|
||||||
|
/// No description provided for @contactsSettings_overwriteOldestSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'When the contact list is full, the oldest non-favorited contact will be replaced.'**
|
||||||
|
String get contactsSettings_overwriteOldestSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @discoveredContacts_Title.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Discovered Contacts'**
|
||||||
|
String get discoveredContacts_Title;
|
||||||
|
|
||||||
|
/// No description provided for @discoveredContacts_noMatching.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No matching contacts'**
|
||||||
|
String get discoveredContacts_noMatching;
|
||||||
|
|
||||||
|
/// No description provided for @discoveredContacts_searchHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Search discovered contacts'**
|
||||||
|
String get discoveredContacts_searchHint;
|
||||||
|
|
||||||
|
/// No description provided for @discoveredContacts_contactAdded.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Contact added'**
|
||||||
|
String get discoveredContacts_contactAdded;
|
||||||
|
|
||||||
|
/// No description provided for @discoveredContacts_addContact.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Add Contact'**
|
||||||
|
String get discoveredContacts_addContact;
|
||||||
|
|
||||||
|
/// No description provided for @discoveredContacts_copyContact.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Copy Contact to clipboard'**
|
||||||
|
String get discoveredContacts_copyContact;
|
||||||
|
|
||||||
|
/// No description provided for @discoveredContacts_deleteContact.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Delete Discovered Contact'**
|
||||||
|
String get discoveredContacts_deleteContact;
|
||||||
|
|
||||||
|
/// No description provided for @discoveredContacts_deleteContactAll.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Delete All Discovered Contacts'**
|
||||||
|
String get discoveredContacts_deleteContactAll;
|
||||||
|
|
||||||
|
/// No description provided for @discoveredContacts_deleteContactAllContent.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Are you sure you want to delete all discovered contacts?'**
|
||||||
|
String get discoveredContacts_deleteContactAllContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get common_delete => 'Изтрий';
|
String get common_delete => 'Изтрий';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get common_deleteAll => 'Изтрий всичко';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get common_close => 'Затвори';
|
String get common_close => 'Затвори';
|
||||||
|
|
||||||
@@ -108,6 +111,90 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
String get scanner_title => 'MeshCore Open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceUsbLabel => 'USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Свържете се чрез USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Изберете открития сериен уред и свържете директно към вашия MeshCore възел.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Изберете USB устройство';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'Няма открити USB устройства. Включете едно и опитайте отново.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied => 'Не беше разрешено достъпът през USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing =>
|
||||||
|
'Избраното USB устройство вече не е налично.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Изберете валитно USB устройство.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy =>
|
||||||
|
'Друг мол за свързване през USB вече е в процес на изпълнение.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'Няма свързано USB устройство.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed =>
|
||||||
|
'Не успях да отворя избраното USB устройство.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'Не успях да се свържа с избраното USB устройство.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'USB серийната комуникация не се поддържа на тази платформа.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'USB връзката вече е активирана.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected => 'Няма избран USB устройство.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'USB връзката не е активна.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName =>
|
||||||
|
'Устройство за четене на уеб серийни данни';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Изберете USB устройство';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Свързване към USB устройство...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Търсене на USB устройства...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'Неуспешно свързване през USB: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Сканиране за устройства...';
|
String get scanner_scanning => 'Сканиране за устройства...';
|
||||||
|
|
||||||
@@ -150,6 +237,13 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
String get scanner_bluetoothOffMessage =>
|
String get scanner_bluetoothOffMessage =>
|
||||||
'Моля, активирайте Bluetooth, за да сканирате за устройства.';
|
'Моля, активирайте Bluetooth, за да сканирате за устройства.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequired => 'Изисква се браузър Chrome';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequiredMessage =>
|
||||||
|
'Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_enableBluetooth => 'Активирайте Bluetooth';
|
String get scanner_enableBluetooth => 'Активирайте Bluetooth';
|
||||||
|
|
||||||
@@ -234,6 +328,13 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_longitude => 'Дължина';
|
String get settings_longitude => 'Дължина';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettings => 'Настройки за контакти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettingsSubtitle =>
|
||||||
|
'Настройки за добавяне на контакти.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_privacyMode => 'Режим на поверителност';
|
String get settings_privacyMode => 'Режим на поверителност';
|
||||||
|
|
||||||
@@ -326,6 +427,10 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
String get settings_aboutDescription =>
|
String get settings_aboutDescription =>
|
||||||
'Отворен софтуер за Flutter клиент за MeshCore LoRa мрежови устройства.';
|
'Отворен софтуер за Flutter клиент за MeshCore LoRa мрежови устройства.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_aboutOpenMeteoAttribution =>
|
||||||
|
'Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_infoName => 'Име';
|
String get settings_infoName => 'Име';
|
||||||
|
|
||||||
@@ -462,6 +567,14 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageUk => 'Украински';
|
String get appSettings_languageUk => 'Украински';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracing =>
|
||||||
|
'Разрешаване на проследяване на съобщения';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracingSubtitle =>
|
||||||
|
'Показване на подробни метаданни за маршрутизация и синхронизация за съобщения';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Уведомления';
|
String get appSettings_notifications => 'Уведомления';
|
||||||
|
|
||||||
@@ -622,6 +735,15 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_offlineMapCache => 'Кеш на офлайн карти';
|
String get appSettings_offlineMapCache => 'Кеш на офлайн карти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsTitle => 'единици';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsMetric => 'Метрика (m / km)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsImperial => 'Имперска (ft / mi)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_noAreaSelected => 'Няма избрана област';
|
String get appSettings_noAreaSelected => 'Няма избрана област';
|
||||||
|
|
||||||
@@ -660,7 +782,35 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
'Контактите ще се появят, когато устройствата рекламират.';
|
'Контактите ще се появят, когато устройствата рекламират.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_searchContacts => 'Търсене на контакти...';
|
String get contacts_unread => 'Непрочетено';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_searchContactsNoNumber => 'Търси контакти...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchContacts(int number, String str) {
|
||||||
|
return 'Търсене на контакти...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchFavorites(int number, String str) {
|
||||||
|
return 'Търсене на $number$str любими...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchUsers(int number, String str) {
|
||||||
|
return 'Търсене на $number$str потребители...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRepeaters(int number, String str) {
|
||||||
|
return 'Търсене на $number$str повтарящи се...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRoomServers(int number, String str) {
|
||||||
|
return 'Търсене на $number$str сървъри в стаята...';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_noUnreadContacts => 'Няма непрочетени контакти';
|
String get contacts_noUnreadContacts => 'Няма непрочетени контакти';
|
||||||
@@ -785,6 +935,12 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_editChannel => 'Редактирай канал';
|
String get channels_editChannel => 'Редактирай канал';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_muteChannel => 'Заглуши канала';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_unmuteChannel => 'Включи известията на канала';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_deleteChannel => 'Изтрий канала';
|
String get channels_deleteChannel => 'Изтрий канала';
|
||||||
|
|
||||||
@@ -793,6 +949,11 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
return 'Изтрий \"$name\"? Това не може да бъде отменено.';
|
return 'Изтрий \"$name\"? Това не може да бъде отменено.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channels_channelDeleteFailed(String name) {
|
||||||
|
return 'Неуспешно изтриване на канала \"$name\"';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String channels_channelDeleted(String name) {
|
String channels_channelDeleted(String name) {
|
||||||
return 'Каналът \"$name\" е изтрит';
|
return 'Каналът \"$name\" е изтрит';
|
||||||
@@ -1080,6 +1241,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_pathManagement => 'Управление на пътища';
|
String get chat_pathManagement => 'Управление на пътища';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_ShowAllPaths => 'Покажи всички пътища';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_routingMode => 'Режим на маршрутизиране';
|
String get chat_routingMode => 'Режим на маршрутизиране';
|
||||||
|
|
||||||
@@ -1240,6 +1404,12 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_title => 'Карта на възлите';
|
String get map_title => 'Карта на възлите';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_lineOfSight => 'Линия на видимост';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_losScreenTitle => 'Линия на видимост';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_noNodesWithLocation => 'Няма възли с данни за местоположение.';
|
String get map_noNodesWithLocation => 'Няма възли с данни за местоположение.';
|
||||||
|
|
||||||
@@ -1357,6 +1527,13 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_showSharedMarkers => 'Покажи споделени маркери';
|
String get map_showSharedMarkers => 'Покажи споделени маркери';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showGuessedLocations =>
|
||||||
|
'Покажете местоположенията на предположените възли.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_guessedLocation => 'Предполагано местоположение';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_lastSeenTime => 'Последна видяна дата';
|
String get map_lastSeenTime => 'Последна видяна дата';
|
||||||
|
|
||||||
@@ -1677,10 +1854,10 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Изпрати команди към ретранслатора';
|
String get repeater_cliSubtitle => 'Изпрати команди към ретранслатора';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Съседи';
|
String get repeater_neighbors => 'Съседи';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle =>
|
String get repeater_neighborsSubtitle =>
|
||||||
'Преглед на съседни възли с нулев скок.';
|
'Преглед на съседни възли с нулев скок.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -2380,7 +2557,7 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Повторители Съседи';
|
String get neighbors_repeatersNeighbors => 'Повторители Съседи';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Няма налични данни за съседи.';
|
String get neighbors_noData => 'Няма налични данни за съседи.';
|
||||||
@@ -2687,6 +2864,15 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get listFilter_all => 'Всички';
|
String get listFilter_all => 'Всички';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_favorites => 'Любими';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_addToFavorites => 'Добави към любими';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_removeFromFavorites => 'Премахване от списъка с любими';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_users => 'Потребители';
|
String get listFilter_users => 'Потребители';
|
||||||
|
|
||||||
@@ -2721,6 +2907,144 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get pathTrace_clearTooltip => 'Изчисти пътя';
|
String get pathTrace_clearTooltip => 'Изчисти пътя';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectStartEnd => 'Изберете начални и крайни възли за LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losRunFailed(String error) {
|
||||||
|
return 'Проверката на пряката видимост е неуспешна: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losClearAllPoints => 'Изчистете всички точки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRunToViewElevationProfile =>
|
||||||
|
'Стартирайте LOS, за да видите профила на надморската височина';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuTitle => 'LOS меню';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuSubtitle =>
|
||||||
|
'Докоснете възли или натиснете продължително карта за персонализирани точки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowDisplayNodes => 'Показване на възли на дисплея';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losCustomPoints => 'Персонализирани точки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losCustomPointLabel(int index) {
|
||||||
|
return 'Персонализирано $index';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointA => 'Точка А';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointB => 'Точка Б';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaA(String value, String unit) {
|
||||||
|
return 'Антена A: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaB(String value, String unit) {
|
||||||
|
return 'Антена B: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRun => 'Стартирайте LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losNoElevationData => 'Няма данни за надморска височина';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileClear(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String clearance,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, чист LOS, минимално разстояние $clearance $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileBlocked(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, блокиран от $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusChecking => 'LOS: проверка...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusNoData => 'LOS: няма данни';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||||
|
return 'LOS: $clear/$total ясно, $blocked блокирано, $unknown неизвестно';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorElevationUnavailable =>
|
||||||
|
'Няма налични данни за надморска височина за една или повече проби.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorInvalidInput =>
|
||||||
|
'Невалидни данни за точки/надморска височина за изчисляване на LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRenameCustomPoint => 'Преименувайте персонализирана точка';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointName => 'Име на точката';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowPanelTooltip => 'Показване на LOS панел';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losHidePanelTooltip => 'Скриване на LOS панела';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losElevationAttribution =>
|
||||||
|
'Данни за надморска височина: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendRadioHorizon => 'Радиохоризонт';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendLosBeam => 'Линия на видимост';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendTerrain => 'Терен';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyLabel => 'Честота';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyInfoTooltip => 'Преглед на детайли за изчислението';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyDialogTitle => 'Изчисляване на радиохоризонта';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losFrequencyDialogDescription(
|
||||||
|
double baselineK,
|
||||||
|
double baselineFreq,
|
||||||
|
double frequencyMHz,
|
||||||
|
double kFactor,
|
||||||
|
) {
|
||||||
|
return 'Започвайки от k=$baselineK при $baselineFreq MHz, изчислението коригира k-фактора за текущата $frequencyMHz MHz лента, която определя границата на извития радиохоризонт.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_pathTrace => 'Пътен проследяване';
|
String get contacts_pathTrace => 'Пътен проследяване';
|
||||||
|
|
||||||
@@ -2890,4 +3214,88 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open износ на данни за карта в формат GPX';
|
'meshcore-open износ на данни за карта в формат GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Близки повтарящи се устройства';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Последно видян';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_title => 'Настройки на контактите';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddTitle => 'Автоматично откриване';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_otherTitle =>
|
||||||
|
'Други настройки свързани с контакти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersTitle =>
|
||||||
|
'Автоматично добавяне на потребители';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersSubtitle =>
|
||||||
|
'Позволи на спътника да добавя автоматично откритите потребители.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersTitle =>
|
||||||
|
'Автоматично добавяне на повтарящи се елементи';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||||
|
'Позволи на спътника да добавя автоматично откритите повтарящи се устройства.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersTitle =>
|
||||||
|
'Автоматично добавяне на сървъри на стаите';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||||
|
'Позволи на спътника да добавя автоматично откритите сървъри на стаите.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsTitle =>
|
||||||
|
'Автоматично добавяне на датчици';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||||
|
'Позволи на спътника да добавя автоматично откритите датчици.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestTitle => 'Премахни най-старото';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestSubtitle =>
|
||||||
|
'Когато списъкът с контакти е пълен, най-старият неключов контакт ще бъде заменен.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_Title => 'Открити контакти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_noMatching => 'Няма съвпадащи контакти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_searchHint => 'Търсене на открити контакти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_contactAdded => 'Контакт добавен';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_addContact => 'Добави контакт';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_copyContact => 'Копирай контакт в клипборда';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContact => 'Изтрий контакт';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAll =>
|
||||||
|
'Изтриване на Всички Открити Контакти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAllContent =>
|
||||||
|
'Сигурни ли сте, че искате да изтриете всички открити контакти?';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get common_delete => 'Löschen';
|
String get common_delete => 'Löschen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get common_deleteAll => 'Alles löschen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get common_close => 'Schließen';
|
String get common_close => 'Schließen';
|
||||||
|
|
||||||
@@ -108,6 +111,91 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
String get scanner_title => 'MeshCore Open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceUsbLabel => 'USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Verbinden über USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Wählen Sie ein USB-Gerät aus';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'Die USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied =>
|
||||||
|
'Die USB-Berechtigung wurde abgelehnt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing =>
|
||||||
|
'Das ausgewählte USB-Gerät ist nicht mehr verfügbar.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Wählen Sie ein gültiges USB-Gerät aus.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy =>
|
||||||
|
'Eine weitere Anfrage für eine USB-Verbindung ist bereits in Bearbeitung.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'Es ist kein USB-Gerät angeschlossen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed =>
|
||||||
|
'Fehlgeschlagen beim Öffnen des ausgewählten USB-Geräts.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'Keine Verbindung zum ausgewählten USB-Gerät hergestellt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'Die USB-Serielle Schnittstelle wird auf dieser Plattform nicht unterstützt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive =>
|
||||||
|
'Eine USB-Verbindung ist bereits hergestellt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected => 'Kein USB-Gerät wurde ausgewählt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'Die USB-Verbindung ist nicht aktiv.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName => 'Web-Serielle Geräte';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Wählen Sie ein USB-Gerät aus';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Verbindung zum USB-Gerät...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Suche nach USB-Geräten...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'Fehler beim USB-Verbindungsaufbau: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Scannen nach Geräten...';
|
String get scanner_scanning => 'Scannen nach Geräten...';
|
||||||
|
|
||||||
@@ -150,6 +238,13 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get scanner_bluetoothOffMessage =>
|
String get scanner_bluetoothOffMessage =>
|
||||||
'Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.';
|
'Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequired => 'Chrome Browser erforderlich';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequiredMessage =>
|
||||||
|
'Diese Webanwendung erfordert Google Chrome oder einen Chromium-basierten Browser für die Bluetooth-Unterstützung.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_enableBluetooth => 'Bluetooth aktivieren';
|
String get scanner_enableBluetooth => 'Bluetooth aktivieren';
|
||||||
|
|
||||||
@@ -233,6 +328,13 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_longitude => 'Längengrad';
|
String get settings_longitude => 'Längengrad';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettings => 'Kontakteinstellungen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettingsSubtitle =>
|
||||||
|
'Einstellungen für das Hinzufügen von Kontakten';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_privacyMode => 'Privatsphäreeinstellung';
|
String get settings_privacyMode => 'Privatsphäreeinstellung';
|
||||||
|
|
||||||
@@ -320,6 +422,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get settings_aboutDescription =>
|
String get settings_aboutDescription =>
|
||||||
'Ein Open-Source-Flutter-Client für MeshCore LoRa-Meshnetzwerkgeräte.';
|
'Ein Open-Source-Flutter-Client für MeshCore LoRa-Meshnetzwerkgeräte.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_aboutOpenMeteoAttribution =>
|
||||||
|
'LOS-Höhendaten: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_infoName => 'Name';
|
String get settings_infoName => 'Name';
|
||||||
|
|
||||||
@@ -456,6 +562,14 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageUk => 'Ukrainisch';
|
String get appSettings_languageUk => 'Ukrainisch';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracing =>
|
||||||
|
'Nachrichtenverfolgung aktivieren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracingSubtitle =>
|
||||||
|
'Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Benachrichtigungen';
|
String get appSettings_notifications => 'Benachrichtigungen';
|
||||||
|
|
||||||
@@ -619,6 +733,15 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_offlineMapCache => 'Offline-Karten-Cache';
|
String get appSettings_offlineMapCache => 'Offline-Karten-Cache';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsTitle => 'Einheiten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsMetric => 'Metrisch (m/km)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsImperial => 'Imperial (ft/mi)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_noAreaSelected => 'Kein Bereich ausgewählt';
|
String get appSettings_noAreaSelected => 'Kein Bereich ausgewählt';
|
||||||
|
|
||||||
@@ -656,7 +779,35 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
'Kontakte werden angezeigt, wenn Geräte eine Ankündigung machen.';
|
'Kontakte werden angezeigt, wenn Geräte eine Ankündigung machen.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_searchContacts => 'Suche Kontakte...';
|
String get contacts_unread => 'Ungelesen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_searchContactsNoNumber => 'Kontakte suchen...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchContacts(int number, String str) {
|
||||||
|
return 'Suche Kontakte...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchFavorites(int number, String str) {
|
||||||
|
return 'Suche $number$str Favoriten...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchUsers(int number, String str) {
|
||||||
|
return 'Suche $number$str Benutzer...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRepeaters(int number, String str) {
|
||||||
|
return 'Suche $number$str Repeater...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRoomServers(int number, String str) {
|
||||||
|
return 'Suche $number$str Raumserver...';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_noUnreadContacts => 'Keine ungesehene Kontakte';
|
String get contacts_noUnreadContacts => 'Keine ungesehene Kontakte';
|
||||||
@@ -721,27 +872,27 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get contacts_noMembers => 'Keine Mitglieder';
|
String get contacts_noMembers => 'Keine Mitglieder';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_lastSeenNow => 'gerade gesehen';
|
String get contacts_lastSeenNow => 'kürzlich';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_lastSeenMinsAgo(int minutes) {
|
String contacts_lastSeenMinsAgo(int minutes) {
|
||||||
return 'Letzte Sichtung vor $minutes Minuten.';
|
return '~ $minutes Min.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_lastSeenHourAgo => 'Letzte Sichtung vor 1 Stunde.';
|
String get contacts_lastSeenHourAgo => '~ 1 Std.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_lastSeenHoursAgo(int hours) {
|
String contacts_lastSeenHoursAgo(int hours) {
|
||||||
return 'Letzte Sichtung vor $hours Stunden.';
|
return '~ $hours Std.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_lastSeenDayAgo => 'Letzte Sichtung vor 1 Tag';
|
String get contacts_lastSeenDayAgo => '~ 1 Tag';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_lastSeenDaysAgo(int days) {
|
String contacts_lastSeenDaysAgo(int days) {
|
||||||
return 'Letzte Sichtung $days Tage zuvor';
|
return '~ $days Tage';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -782,6 +933,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_editChannel => 'Kanal bearbeiten';
|
String get channels_editChannel => 'Kanal bearbeiten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_muteChannel => 'Kanal stummschalten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_unmuteChannel => 'Kanal Stummschaltung aufheben';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_deleteChannel => 'Lösche den Kanal';
|
String get channels_deleteChannel => 'Lösche den Kanal';
|
||||||
|
|
||||||
@@ -790,6 +947,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
return 'Löschen von \"$name\"? Dies kann nicht rückgängig gemacht werden.';
|
return 'Löschen von \"$name\"? Dies kann nicht rückgängig gemacht werden.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channels_channelDeleteFailed(String name) {
|
||||||
|
return 'Kanal $name konnte nicht gelöscht werden';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String channels_channelDeleted(String name) {
|
String channels_channelDeleted(String name) {
|
||||||
return 'Kanal \"$name\" gelöscht';
|
return 'Kanal \"$name\" gelöscht';
|
||||||
@@ -1080,6 +1242,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_pathManagement => 'Pfadverwaltung';
|
String get chat_pathManagement => 'Pfadverwaltung';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_ShowAllPaths => 'Alle Pfade anzeigen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_routingMode => 'Routenmodus';
|
String get chat_routingMode => 'Routenmodus';
|
||||||
|
|
||||||
@@ -1239,6 +1404,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_title => 'Karte';
|
String get map_title => 'Karte';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_lineOfSight => 'Sichtlinie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_losScreenTitle => 'Sichtlinie';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_noNodesWithLocation => 'Keine Knoten mit Standortdaten';
|
String get map_noNodesWithLocation => 'Keine Knoten mit Standortdaten';
|
||||||
|
|
||||||
@@ -1356,6 +1527,13 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_showSharedMarkers => 'Zeige gemeinsam genutzte Marker';
|
String get map_showSharedMarkers => 'Zeige gemeinsam genutzte Marker';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showGuessedLocations =>
|
||||||
|
'Zeige die vermuteten Knotenpositionen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_guessedLocation => 'Geschätzter Ort';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_lastSeenTime => 'Letzte Sichtung';
|
String get map_lastSeenTime => 'Letzte Sichtung';
|
||||||
|
|
||||||
@@ -1676,10 +1854,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Sende Befehle an den Repeater';
|
String get repeater_cliSubtitle => 'Sende Befehle an den Repeater';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Nachbarn';
|
String get repeater_neighbors => 'Nachbarn';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle => 'Anzahl der Hop-Nachbarn anzeigen.';
|
String get repeater_neighborsSubtitle => 'Anzahl der Hop-Nachbarn anzeigen.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => 'Einstellungen';
|
String get repeater_settings => 'Einstellungen';
|
||||||
@@ -2382,7 +2560,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Nachbarn';
|
String get neighbors_repeatersNeighbors => 'Nachbarn';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Keine Nachbarsdaten verfügbar.';
|
String get neighbors_noData => 'Keine Nachbarsdaten verfügbar.';
|
||||||
@@ -2692,6 +2870,15 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get listFilter_all => 'Alle';
|
String get listFilter_all => 'Alle';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_favorites => 'Favoriten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_addToFavorites => 'Zu Favoriten hinzufügen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_removeFromFavorites => 'Aus Favoriten entfernen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_users => 'Benutzer';
|
String get listFilter_users => 'Benutzer';
|
||||||
|
|
||||||
@@ -2726,6 +2913,145 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get pathTrace_clearTooltip => 'Pfad löschen';
|
String get pathTrace_clearTooltip => 'Pfad löschen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectStartEnd =>
|
||||||
|
'Wählen Sie Start- und Endknoten für LOS aus.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losRunFailed(String error) {
|
||||||
|
return 'Sichtlinienprüfung fehlgeschlagen: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losClearAllPoints => 'Löschen Sie alle Punkte';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRunToViewElevationProfile =>
|
||||||
|
'Führen Sie LOS aus, um das Höhenprofil anzuzeigen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuTitle => 'LOS-Menü';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuSubtitle =>
|
||||||
|
'Tippen Sie auf Knoten oder drücken Sie lange auf die Karte, um benutzerdefinierte Punkte anzuzeigen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowDisplayNodes => 'Anzeigeknoten anzeigen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losCustomPoints => 'Benutzerdefinierte Punkte';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losCustomPointLabel(int index) {
|
||||||
|
return 'Benutzerdefiniert $index';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointA => 'Punkt A';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointB => 'Punkt B';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaA(String value, String unit) {
|
||||||
|
return 'Antenne A: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaB(String value, String unit) {
|
||||||
|
return 'Antenne B: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRun => 'Führen Sie LOS aus';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losNoElevationData => 'Keine Höhendaten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileClear(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String clearance,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, freie Sichtlinie, Mindestabstand $clearance $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileBlocked(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, blockiert durch $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusChecking => 'LOS: Überprüfen...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusNoData => 'LOS: keine Daten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||||
|
return 'Sichtlinie: $clear/$total frei, $blocked blockiert, $unknown unbekannt';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorElevationUnavailable =>
|
||||||
|
'Für eine oder mehrere Proben sind keine Höhendaten verfügbar.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorInvalidInput =>
|
||||||
|
'Ungültige Punkte/Höhendaten für die LOS-Berechnung.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRenameCustomPoint =>
|
||||||
|
'Benennen Sie den benutzerdefinierten Punkt um';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointName => 'Punktname';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowPanelTooltip => 'LOS-Panel anzeigen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losHidePanelTooltip => 'LOS-Panel ausblenden';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losElevationAttribution => 'Höhendaten: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendRadioHorizon => 'Funkhorizont';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendLosBeam => 'Sichtlinie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendTerrain => 'Gelände';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyLabel => 'Frequenz';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyInfoTooltip => 'Details zur Berechnung anzeigen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyDialogTitle => 'Berechnung des Funkhorizonts';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losFrequencyDialogDescription(
|
||||||
|
double baselineK,
|
||||||
|
double baselineFreq,
|
||||||
|
double frequencyMHz,
|
||||||
|
double kFactor,
|
||||||
|
) {
|
||||||
|
return 'Ausgehend von k=$baselineK bei $baselineFreq MHz passt die Berechnung den k-Faktor für das aktuelle $frequencyMHz MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_pathTrace => 'Pfadverfolgung';
|
String get contacts_pathTrace => 'Pfadverfolgung';
|
||||||
|
|
||||||
@@ -2898,4 +3224,90 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'GPX-Kartendaten aus meshcore-open exportieren';
|
'GPX-Kartendaten aus meshcore-open exportieren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'In der Nähe befindliche Repeater';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Zuletzt gesehen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_title => 'Kontakteinstellungen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddTitle => 'Automatische Erkennung';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_otherTitle =>
|
||||||
|
'Weitere Einstellungen zu Kontakten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersTitle =>
|
||||||
|
'Automatische Hinzufügung von Benutzern';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersSubtitle =>
|
||||||
|
'Ermöglichen Sie dem Begleiter, automatisch entdeckte Benutzer hinzuzufügen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersTitle =>
|
||||||
|
'Automatisch Repeater hinzufügen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||||
|
'Ermöglichen Sie dem Begleiter, automatisch entdeckte Repeater hinzuzufügen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersTitle =>
|
||||||
|
'Automatisch Raumservers hinzufügen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||||
|
'Ermöglichen Sie dem Begleiter, entdeckte Raumserver automatisch hinzuzufügen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsTitle =>
|
||||||
|
'Automatisch Sensoren hinzufügen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||||
|
'Ermöglichen Sie dem Begleiter, automatisch entdeckte Sensoren hinzuzufügen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestTitle =>
|
||||||
|
'Überschreiben des Ältesten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestSubtitle =>
|
||||||
|
'Wenn die Kontaktliste voll ist, wird der älteste nicht favorisierte Kontakt ersetzt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_Title => 'Entdeckte Kontakte';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_noMatching => 'Keine passenden Kontakte';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_searchHint => 'Entdeckte Kontakte suchen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_contactAdded => 'Kontakt hinzugefügt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_addContact => 'Kontakt hinzufügen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_copyContact =>
|
||||||
|
'Kontakt in die Zwischenablage kopieren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContact => 'Kontakt löschen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAll =>
|
||||||
|
'Alle entdeckten Kontakte löschen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAllContent =>
|
||||||
|
'Sind Sie sicher, dass Sie alle gefundenen Kontakte löschen möchten?';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get common_delete => 'Delete';
|
String get common_delete => 'Delete';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get common_deleteAll => 'Delete All';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get common_close => 'Close';
|
String get common_close => 'Close';
|
||||||
|
|
||||||
@@ -108,6 +111,88 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
String get scanner_title => 'MeshCore Open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceUsbLabel => 'USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Connect over USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Choose a detected serial device and connect directly to your MeshCore node.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Select a USB device';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'USB serial is active on supported Android devices and desktop platforms.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'No USB devices found. Plug one in and refresh.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied => 'USB permission was denied.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing =>
|
||||||
|
'The selected USB device is no longer available.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Select a valid USB device.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy =>
|
||||||
|
'Another USB connection request is already in progress.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'No USB device is connected.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed => 'Failed to open the selected USB device.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'Failed to connect to the selected USB device.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'USB serial is not supported on this platform.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'A USB connection is already active.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected => 'No USB device was selected.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'The USB connection is not open.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'Connection timed out. Make sure the device has USB Companion firmware.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName => 'Web Serial Device';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Select a USB device';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Connecting to USB device...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Searching for USB devices...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'USB connection failed: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Scanning for devices...';
|
String get scanner_scanning => 'Scanning for devices...';
|
||||||
|
|
||||||
@@ -149,6 +234,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
String get scanner_bluetoothOffMessage =>
|
String get scanner_bluetoothOffMessage =>
|
||||||
'Please turn on Bluetooth to scan for devices';
|
'Please turn on Bluetooth to scan for devices';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequired => 'Chrome Browser Required';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequiredMessage =>
|
||||||
|
'This web application requires Google Chrome or a Chromium-based browser for Bluetooth support.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_enableBluetooth => 'Enable Bluetooth';
|
String get scanner_enableBluetooth => 'Enable Bluetooth';
|
||||||
|
|
||||||
@@ -232,6 +324,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_longitude => 'Longitude';
|
String get settings_longitude => 'Longitude';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettings => 'Contact Settings';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettingsSubtitle =>
|
||||||
|
'Settings for how contacts are added.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_privacyMode => 'Privacy Mode';
|
String get settings_privacyMode => 'Privacy Mode';
|
||||||
|
|
||||||
@@ -318,6 +417,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
String get settings_aboutDescription =>
|
String get settings_aboutDescription =>
|
||||||
'An open-source Flutter client for MeshCore LoRa mesh networking devices.';
|
'An open-source Flutter client for MeshCore LoRa mesh networking devices.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_aboutOpenMeteoAttribution =>
|
||||||
|
'LOS elevation data: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_infoName => 'Name';
|
String get settings_infoName => 'Name';
|
||||||
|
|
||||||
@@ -454,6 +557,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageUk => 'Українська';
|
String get appSettings_languageUk => 'Українська';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracing => 'Enable Message Tracing';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracingSubtitle =>
|
||||||
|
'Show detailed routing and timing metadata for messages';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Notifications';
|
String get appSettings_notifications => 'Notifications';
|
||||||
|
|
||||||
@@ -614,6 +724,15 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_offlineMapCache => 'Offline Map Cache';
|
String get appSettings_offlineMapCache => 'Offline Map Cache';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsTitle => 'Units';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsMetric => 'Metric (m / km)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsImperial => 'Imperial (ft / mi)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_noAreaSelected => 'No area selected';
|
String get appSettings_noAreaSelected => 'No area selected';
|
||||||
|
|
||||||
@@ -650,7 +769,35 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
'Contacts will appear when devices advertise';
|
'Contacts will appear when devices advertise';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_searchContacts => 'Search contacts...';
|
String get contacts_unread => 'Unread';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_searchContactsNoNumber => 'Search Contacts...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchContacts(int number, String str) {
|
||||||
|
return 'Search $number$str Contacts...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchFavorites(int number, String str) {
|
||||||
|
return 'Search $number$str Favorites...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchUsers(int number, String str) {
|
||||||
|
return 'Search $number$str Users...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRepeaters(int number, String str) {
|
||||||
|
return 'Search $number$str Repeaters...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRoomServers(int number, String str) {
|
||||||
|
return 'Search $number$str Room servers...';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_noUnreadContacts => 'No unread contacts';
|
String get contacts_noUnreadContacts => 'No unread contacts';
|
||||||
@@ -713,27 +860,27 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
String get contacts_noMembers => 'No members';
|
String get contacts_noMembers => 'No members';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_lastSeenNow => 'Last seen now';
|
String get contacts_lastSeenNow => 'recently';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_lastSeenMinsAgo(int minutes) {
|
String contacts_lastSeenMinsAgo(int minutes) {
|
||||||
return 'Last seen $minutes mins ago';
|
return '~ $minutes min.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_lastSeenHourAgo => 'Last seen 1 hour ago';
|
String get contacts_lastSeenHourAgo => '~ 1 hour';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_lastSeenHoursAgo(int hours) {
|
String contacts_lastSeenHoursAgo(int hours) {
|
||||||
return 'Last seen $hours hours ago';
|
return '~ $hours hours';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_lastSeenDayAgo => 'Last seen 1 day ago';
|
String get contacts_lastSeenDayAgo => '~ 1 day';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_lastSeenDaysAgo(int days) {
|
String contacts_lastSeenDaysAgo(int days) {
|
||||||
return 'Last seen $days days ago';
|
return '~ $days days';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -774,6 +921,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_editChannel => 'Edit channel';
|
String get channels_editChannel => 'Edit channel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_muteChannel => 'Mute channel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_unmuteChannel => 'Unmute channel';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_deleteChannel => 'Delete channel';
|
String get channels_deleteChannel => 'Delete channel';
|
||||||
|
|
||||||
@@ -782,6 +935,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
return 'Delete \"$name\"? This cannot be undone.';
|
return 'Delete \"$name\"? This cannot be undone.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channels_channelDeleteFailed(String name) {
|
||||||
|
return 'Failed to delete channel \"$name\"';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String channels_channelDeleted(String name) {
|
String channels_channelDeleted(String name) {
|
||||||
return 'Channel \"$name\" deleted';
|
return 'Channel \"$name\" deleted';
|
||||||
@@ -1065,6 +1223,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_pathManagement => 'Path Management';
|
String get chat_pathManagement => 'Path Management';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_ShowAllPaths => 'Show all paths';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_routingMode => 'Routing mode';
|
String get chat_routingMode => 'Routing mode';
|
||||||
|
|
||||||
@@ -1219,6 +1380,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_title => 'Node Map';
|
String get map_title => 'Node Map';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_lineOfSight => 'Line of Sight';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_losScreenTitle => 'Line of Sight';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_noNodesWithLocation => 'No nodes with location data';
|
String get map_noNodesWithLocation => 'No nodes with location data';
|
||||||
|
|
||||||
@@ -1336,6 +1503,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_showSharedMarkers => 'Show shared markers';
|
String get map_showSharedMarkers => 'Show shared markers';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showGuessedLocations => 'Show guessed node locations';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_guessedLocation => 'Guessed location';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_lastSeenTime => 'Last Seen Time';
|
String get map_lastSeenTime => 'Last Seen Time';
|
||||||
|
|
||||||
@@ -1650,10 +1823,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Send commands to the repeater';
|
String get repeater_cliSubtitle => 'Send commands to the repeater';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Neighbors';
|
String get repeater_neighbors => 'Neighbors';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle => 'View zero hop neighbors.';
|
String get repeater_neighborsSubtitle => 'View zero hop neighbors.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => 'Settings';
|
String get repeater_settings => 'Settings';
|
||||||
@@ -2329,10 +2502,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_receivedData => 'Received Neighbours Data';
|
String get neighbors_receivedData => 'Received Neighbors Data';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_requestTimedOut => 'Neighbours request timed out.';
|
String get neighbors_requestTimedOut => 'Neighbors request timed out.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String neighbors_errorLoading(String error) {
|
String neighbors_errorLoading(String error) {
|
||||||
@@ -2340,10 +2513,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Repeaters Neighbours';
|
String get neighbors_repeatersNeighbors => 'Repeaters Neighbors';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'No neighbours data available.';
|
String get neighbors_noData => 'No neighbors data available.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String neighbors_unknownContact(String pubkey) {
|
String neighbors_unknownContact(String pubkey) {
|
||||||
@@ -2646,6 +2819,15 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get listFilter_all => 'All';
|
String get listFilter_all => 'All';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_favorites => 'Favorites';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_addToFavorites => 'Add to favorites';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_removeFromFavorites => 'Remove from favorites';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_users => 'Users';
|
String get listFilter_users => 'Users';
|
||||||
|
|
||||||
@@ -2680,6 +2862,143 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get pathTrace_clearTooltip => 'Clear path.';
|
String get pathTrace_clearTooltip => 'Clear path.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectStartEnd => 'Select start and end nodes for LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losRunFailed(String error) {
|
||||||
|
return 'Line-of-sight check failed: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losClearAllPoints => 'Clear all points';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRunToViewElevationProfile =>
|
||||||
|
'Run LOS to view elevation profile';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuTitle => 'LOS Menu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuSubtitle => 'Tap nodes or long-press map for custom points';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowDisplayNodes => 'Show display nodes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losCustomPoints => 'Custom points';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losCustomPointLabel(int index) {
|
||||||
|
return 'Custom $index';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointA => 'Point A';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointB => 'Point B';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaA(String value, String unit) {
|
||||||
|
return 'Antenna A: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaB(String value, String unit) {
|
||||||
|
return 'Antenna B: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRun => 'Run LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losNoElevationData => 'No elevation data';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileClear(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String clearance,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, clear LOS, min clearance $clearance $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileBlocked(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, blocked by $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusChecking => 'LOS: checking...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusNoData => 'LOS: no data';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||||
|
return 'LOS: $clear/$total clear, $blocked blocked, $unknown unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorElevationUnavailable =>
|
||||||
|
'Elevation data unavailable for one or more samples.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorInvalidInput =>
|
||||||
|
'Invalid points/elevation data for LOS calculation.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRenameCustomPoint => 'Rename custom point';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointName => 'Point name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowPanelTooltip => 'Show LOS panel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losHidePanelTooltip => 'Hide LOS panel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losElevationAttribution =>
|
||||||
|
'Elevation data: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendRadioHorizon => 'Radio horizon';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendLosBeam => 'LOS beam';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendTerrain => 'Terrain';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyLabel => 'Frequency';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyInfoTooltip => 'View calculation details';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyDialogTitle => 'Radio horizon calculation';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losFrequencyDialogDescription(
|
||||||
|
double baselineK,
|
||||||
|
double baselineFreq,
|
||||||
|
double frequencyMHz,
|
||||||
|
double kFactor,
|
||||||
|
) {
|
||||||
|
return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation adjusts the k-factor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_pathTrace => 'Path Trace';
|
String get contacts_pathTrace => 'Path Trace';
|
||||||
|
|
||||||
@@ -2845,4 +3164,84 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open GPX map data export';
|
'meshcore-open GPX map data export';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Nearby Repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Last seen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_title => 'Contacts settings';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddTitle => 'Automatic Discovery';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_otherTitle => 'Other contact related settings';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersTitle => 'Auto-add users';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersSubtitle =>
|
||||||
|
'Allow the companion to automatically add discovered users.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||||
|
'Allow the companion to automatically add discovered repeaters.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersTitle =>
|
||||||
|
'Auto-add room servers';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||||
|
'Allow the companion to automatically add discovered room servers.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||||
|
'Allow the companion to automatically add discovered sensors.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestSubtitle =>
|
||||||
|
'When the contact list is full, the oldest non-favorited contact will be replaced.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_Title => 'Discovered Contacts';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_noMatching => 'No matching contacts';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_searchHint => 'Search discovered contacts';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_contactAdded => 'Contact added';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_addContact => 'Add Contact';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_copyContact => 'Copy Contact to clipboard';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContact => 'Delete Discovered Contact';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAll =>
|
||||||
|
'Delete All Discovered Contacts';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAllContent =>
|
||||||
|
'Are you sure you want to delete all discovered contacts?';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get common_delete => 'Eliminar';
|
String get common_delete => 'Eliminar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get common_deleteAll => 'Eliminar todo';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get common_close => 'Cerrar';
|
String get common_close => 'Cerrar';
|
||||||
|
|
||||||
@@ -108,6 +111,91 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
String get scanner_title => 'MeshCore Open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceUsbLabel => 'USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Conecte mediante USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Seleccione el dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Seleccione un dispositivo USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'No se encontraron dispositivos USB. Conecte uno y vuelva a intentar.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied =>
|
||||||
|
'Se denegó el permiso de acceso a través de USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing =>
|
||||||
|
'El dispositivo USB seleccionado ya no está disponible.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Seleccione un dispositivo USB válido.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy =>
|
||||||
|
'Ya se ha iniciado una solicitud de conexión USB adicional.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'No hay ningún dispositivo USB conectado.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed =>
|
||||||
|
'No se pudo abrir el dispositivo USB seleccionado.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'No se pudo conectar con el dispositivo USB seleccionado.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'La comunicación serial a través de USB no está soportada en esta plataforma.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'La conexión USB ya está activa.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected =>
|
||||||
|
'No se ha seleccionado ningún dispositivo USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'La conexión USB no está activa.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName => 'Dispositivo de serie web';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Seleccione un dispositivo USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Conectándose al dispositivo USB...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Buscando dispositivos USB...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'Error al conectar mediante USB: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Escaneando dispositivos...';
|
String get scanner_scanning => 'Escaneando dispositivos...';
|
||||||
|
|
||||||
@@ -150,6 +238,13 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
String get scanner_bluetoothOffMessage =>
|
String get scanner_bluetoothOffMessage =>
|
||||||
'Por favor, active el Bluetooth para escanear dispositivos.';
|
'Por favor, active el Bluetooth para escanear dispositivos.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequired => 'Navegador Chrome requerido';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequiredMessage =>
|
||||||
|
'Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_enableBluetooth => 'Habilitar Bluetooth';
|
String get scanner_enableBluetooth => 'Habilitar Bluetooth';
|
||||||
|
|
||||||
@@ -233,6 +328,13 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_longitude => 'Longitud';
|
String get settings_longitude => 'Longitud';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettings => 'Configuración de contacto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettingsSubtitle =>
|
||||||
|
'Configuración de cómo se agregan los contactos.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_privacyMode => 'Modo Privacidad';
|
String get settings_privacyMode => 'Modo Privacidad';
|
||||||
|
|
||||||
@@ -323,6 +425,10 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
String get settings_aboutDescription =>
|
String get settings_aboutDescription =>
|
||||||
'Un cliente de código abierto de Flutter para dispositivos de red mesh LoRa de MeshCore.';
|
'Un cliente de código abierto de Flutter para dispositivos de red mesh LoRa de MeshCore.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_aboutOpenMeteoAttribution =>
|
||||||
|
'Datos de elevación LOS: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_infoName => 'Nombre';
|
String get settings_infoName => 'Nombre';
|
||||||
|
|
||||||
@@ -459,6 +565,14 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageUk => 'Ucraniano';
|
String get appSettings_languageUk => 'Ucraniano';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracing =>
|
||||||
|
'Habilitar seguimiento de mensajes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracingSubtitle =>
|
||||||
|
'Mostrar metadatos detallados de enrutamiento y tiempo para los mensajes';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Notificaciones';
|
String get appSettings_notifications => 'Notificaciones';
|
||||||
|
|
||||||
@@ -620,6 +734,15 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_offlineMapCache => 'Caché de Mapa Offline';
|
String get appSettings_offlineMapCache => 'Caché de Mapa Offline';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsTitle => 'Unidades';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsMetric => 'Métrico (m/km)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsImperial => 'Imperial (pies/millas)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_noAreaSelected => 'No se ha seleccionado ningún área';
|
String get appSettings_noAreaSelected => 'No se ha seleccionado ningún área';
|
||||||
|
|
||||||
@@ -657,7 +780,35 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
'Los contactos aparecerán cuando los dispositivos anuncien.';
|
'Los contactos aparecerán cuando los dispositivos anuncien.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_searchContacts => 'Buscar contactos...';
|
String get contacts_unread => 'No leído';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_searchContactsNoNumber => 'Buscar contactos...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchContacts(int number, String str) {
|
||||||
|
return 'Buscar contactos...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchFavorites(int number, String str) {
|
||||||
|
return 'Buscar $number$str Favoritos...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchUsers(int number, String str) {
|
||||||
|
return 'Buscar $number$str Usuarios...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRepeaters(int number, String str) {
|
||||||
|
return 'Buscar $number$str Repetidores...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRoomServers(int number, String str) {
|
||||||
|
return 'Buscar $number$str servidores de sala...';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_noUnreadContacts => 'No contactos sin leer';
|
String get contacts_noUnreadContacts => 'No contactos sin leer';
|
||||||
@@ -726,23 +877,23 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_lastSeenMinsAgo(int minutes) {
|
String contacts_lastSeenMinsAgo(int minutes) {
|
||||||
return 'Última vez visto hace $minutes minutos.';
|
return '~ $minutes min.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_lastSeenHourAgo => 'Última vez que se vio hace 1 hora';
|
String get contacts_lastSeenHourAgo => '~ 1 hora';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_lastSeenHoursAgo(int hours) {
|
String contacts_lastSeenHoursAgo(int hours) {
|
||||||
return 'Última vez visto hace $hours horas.';
|
return '~ $hours horas';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_lastSeenDayAgo => 'Última vez que se vio hace 1 día';
|
String get contacts_lastSeenDayAgo => '~ 1 día';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_lastSeenDaysAgo(int days) {
|
String contacts_lastSeenDaysAgo(int days) {
|
||||||
return 'Última vez visto hace $days días.';
|
return '~ $days días';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -783,6 +934,12 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_editChannel => 'Editar canal';
|
String get channels_editChannel => 'Editar canal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_muteChannel => 'Silenciar canal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_unmuteChannel => 'Activar canal';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_deleteChannel => 'Eliminar canal';
|
String get channels_deleteChannel => 'Eliminar canal';
|
||||||
|
|
||||||
@@ -791,6 +948,11 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
return 'Eliminar \"$name\"? Esto no se puede deshacer.';
|
return 'Eliminar \"$name\"? Esto no se puede deshacer.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channels_channelDeleteFailed(String name) {
|
||||||
|
return 'No se pudo eliminar el canal \"$name\"';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String channels_channelDeleted(String name) {
|
String channels_channelDeleted(String name) {
|
||||||
return 'Canal \"$name\" eliminado';
|
return 'Canal \"$name\" eliminado';
|
||||||
@@ -1079,6 +1241,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_pathManagement => 'Gestión de Rutas';
|
String get chat_pathManagement => 'Gestión de Rutas';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_ShowAllPaths => 'Mostrar todos los caminos';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_routingMode => 'Modo de enrutamiento';
|
String get chat_routingMode => 'Modo de enrutamiento';
|
||||||
|
|
||||||
@@ -1237,6 +1402,12 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_title => 'Mapa de Nodos';
|
String get map_title => 'Mapa de Nodos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_lineOfSight => 'Línea de visión';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_losScreenTitle => 'Línea de visión';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_noNodesWithLocation => 'No hay nodos con datos de ubicación';
|
String get map_noNodesWithLocation => 'No hay nodos con datos de ubicación';
|
||||||
|
|
||||||
@@ -1354,6 +1525,13 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_showSharedMarkers => 'Mostrar marcadores compartidos';
|
String get map_showSharedMarkers => 'Mostrar marcadores compartidos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showGuessedLocations =>
|
||||||
|
'Mostrar las ubicaciones estimadas de los nodos.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_guessedLocation => 'Ubicación estimada';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_lastSeenTime => 'Última vez que se vio';
|
String get map_lastSeenTime => 'Última vez que se vio';
|
||||||
|
|
||||||
@@ -1674,10 +1852,10 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Enviar comandos al repetidor';
|
String get repeater_cliSubtitle => 'Enviar comandos al repetidor';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Vecinos';
|
String get repeater_neighbors => 'Vecinos';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle => 'Ver vecinos de salto cero.';
|
String get repeater_neighborsSubtitle => 'Ver vecinos de salto cero.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => 'Configuración';
|
String get repeater_settings => 'Configuración';
|
||||||
@@ -2376,7 +2554,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Repetidores Vecinos';
|
String get neighbors_repeatersNeighbors => 'Repetidores Vecinos';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'No hay datos de vecinos disponibles.';
|
String get neighbors_noData => 'No hay datos de vecinos disponibles.';
|
||||||
@@ -2685,6 +2863,15 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get listFilter_all => 'Todas';
|
String get listFilter_all => 'Todas';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_favorites => 'Favoritos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_addToFavorites => 'Añadir a favoritos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_removeFromFavorites => 'Eliminar de las favoritas';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_users => 'Usuarios';
|
String get listFilter_users => 'Usuarios';
|
||||||
|
|
||||||
@@ -2719,6 +2906,146 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get pathTrace_clearTooltip => 'Borrar ruta';
|
String get pathTrace_clearTooltip => 'Borrar ruta';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectStartEnd =>
|
||||||
|
'Seleccione los nodos de inicio y fin para LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losRunFailed(String error) {
|
||||||
|
return 'Error en la comprobación de la línea de visión: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losClearAllPoints => 'Borrar todos los puntos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRunToViewElevationProfile =>
|
||||||
|
'Ejecute LOS para ver el perfil de elevación';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuTitle => 'Menú LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuSubtitle =>
|
||||||
|
'Toque nodos o mantenga presionado el mapa para puntos personalizados';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowDisplayNodes => 'Mostrar nodos de visualización';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losCustomPoints => 'Puntos personalizados';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losCustomPointLabel(int index) {
|
||||||
|
return 'Personalizado $index';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointA => 'Punto A';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointB => 'Punto B';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaA(String value, String unit) {
|
||||||
|
return 'Antena A: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaB(String value, String unit) {
|
||||||
|
return 'Antena B: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRun => 'Ejecutar LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losNoElevationData => 'Sin datos de elevación';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileClear(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String clearance,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, despejar LOS, autorización mínima $clearance $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileBlocked(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, bloqueado por $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusChecking => 'LOS: comprobando...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusNoData => 'LOS: sin datos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||||
|
return 'LOS: $clear/$total claro, $blocked bloqueado, $unknown desconocido';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorElevationUnavailable =>
|
||||||
|
'Datos de elevación no disponibles para una o más muestras.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorInvalidInput =>
|
||||||
|
'Datos de puntos/elevación no válidos para el cálculo de LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRenameCustomPoint =>
|
||||||
|
'Cambiar el nombre del punto personalizado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointName => 'Nombre del punto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowPanelTooltip => 'Mostrar panel LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losHidePanelTooltip => 'Ocultar panel LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losElevationAttribution =>
|
||||||
|
'Datos de elevación: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendRadioHorizon => 'Horizonte radioeléctrico';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendLosBeam => 'Línea de visión';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendTerrain => 'Terreno';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyLabel => 'Frecuencia';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyInfoTooltip => 'Ver detalles del cálculo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyDialogTitle => 'Cálculo del horizonte radioeléctrico';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losFrequencyDialogDescription(
|
||||||
|
double baselineK,
|
||||||
|
double baselineFreq,
|
||||||
|
double frequencyMHz,
|
||||||
|
double kFactor,
|
||||||
|
) {
|
||||||
|
return 'A partir de k=$baselineK en $baselineFreq MHz, el cálculo ajusta el factor k para la banda actual de $frequencyMHz MHz, que define el límite curvo del horizonte de radio.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_pathTrace => 'Rastreo de caminos';
|
String get contacts_pathTrace => 'Rastreo de caminos';
|
||||||
|
|
||||||
@@ -2889,4 +3216,91 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open exportación de datos de mapa GPX';
|
'meshcore-open exportación de datos de mapa GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Repetidores cercanos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Visto por última vez';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_title => 'Configuración de contactos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddTitle => 'Detección automática';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_otherTitle =>
|
||||||
|
'Otras configuraciones relacionadas con el contacto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersTitle =>
|
||||||
|
'Agregar usuarios automáticamente';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersSubtitle =>
|
||||||
|
'Permitir que el compañero agregue automáticamente a los usuarios descubiertos.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersTitle =>
|
||||||
|
'Agregar repetidores automáticamente';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||||
|
'Permitir que el compañero agregue automáticamente los repetidores descubiertos.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersTitle =>
|
||||||
|
'Agregar automáticamente servidores de sala';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||||
|
'Permitir que el compañero agregue automáticamente los servidores de salas descubiertos.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsTitle =>
|
||||||
|
'Agregar sensores automáticamente';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||||
|
'Permitir que el compañero agregue automáticamente los sensores descubiertos.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestTitle =>
|
||||||
|
'Sobreescribir el más antiguo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestSubtitle =>
|
||||||
|
'Cuando la lista de contactos esté llena, se reemplazará el contacto no favorito más antiguo.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_Title => 'Contactos descubiertos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_noMatching =>
|
||||||
|
'No se encontraron contactos coincidentes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_searchHint => 'Buscar contactos descubiertos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_contactAdded => 'Contacto agregado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_addContact => 'Agregar contacto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_copyContact =>
|
||||||
|
'Copiar contacto al portapapeles';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContact => 'Eliminar contacto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAll =>
|
||||||
|
'Eliminar Todos los Contactos Descubiertos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAllContent =>
|
||||||
|
'¿Está seguro de que desea eliminar todos los contactos descubiertos!';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get common_delete => 'Supprimer';
|
String get common_delete => 'Supprimer';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get common_deleteAll => 'Supprimer tout';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get common_close => 'Fermer';
|
String get common_close => 'Fermer';
|
||||||
|
|
||||||
@@ -108,6 +111,91 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
String get scanner_title => 'MeshCore Open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceUsbLabel => 'USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Connectez via USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Sélectionnez un périphérique série détecté et connectez-vous directement à votre nœud MeshCore.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Sélectionnez un périphérique USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'La communication série USB est active sur les appareils Android et les plateformes de bureau compatibles.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'Aucun périphérique USB n\'a été trouvé. Veuillez en brancher un et rafraîchir la page.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied => 'L\'accès via USB a été refusé.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing =>
|
||||||
|
'Le périphérique USB sélectionné n\'est plus disponible.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Sélectionnez un périphérique USB valide.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy =>
|
||||||
|
'Une autre demande de connexion USB est déjà en cours.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'Aucun appareil USB n\'est connecté.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed =>
|
||||||
|
'Impossible d\'ouvrir l\'appareil USB sélectionné.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'Impossible de se connecter à l\'appareil USB sélectionné.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'La communication série USB n\'est pas prise en charge sur cette plateforme.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'Une connexion USB est déjà établie.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected =>
|
||||||
|
'Aucun appareil USB n\'a été sélectionné.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'La connexion USB n\'est pas établie.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'La connexion a expiré. Assurez-vous que l\'appareil dispose du firmware USB Companion.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName =>
|
||||||
|
'Dispositif de communication série sur le Web';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Sélectionnez un périphérique USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Connexion au périphérique USB...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Recherche de périphériques USB...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'Échec de la connexion USB : $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Recherche de périphériques...';
|
String get scanner_scanning => 'Recherche de périphériques...';
|
||||||
|
|
||||||
@@ -150,6 +238,13 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get scanner_bluetoothOffMessage =>
|
String get scanner_bluetoothOffMessage =>
|
||||||
'Veuillez activer le Bluetooth pour rechercher des appareils.';
|
'Veuillez activer le Bluetooth pour rechercher des appareils.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequired => 'Navigateur Chrome requis';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequiredMessage =>
|
||||||
|
'Cette application web nécessite Google Chrome ou un navigateur basé sur Chromium pour le support Bluetooth.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_enableBluetooth => 'Activer le Bluetooth';
|
String get scanner_enableBluetooth => 'Activer le Bluetooth';
|
||||||
|
|
||||||
@@ -234,6 +329,13 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_longitude => 'Longitude';
|
String get settings_longitude => 'Longitude';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettings => 'Paramètres de contact';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettingsSubtitle =>
|
||||||
|
'Paramètres pour l\'ajout de contacts';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_privacyMode => 'Mode de confidentialité';
|
String get settings_privacyMode => 'Mode de confidentialité';
|
||||||
|
|
||||||
@@ -324,6 +426,10 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get settings_aboutDescription =>
|
String get settings_aboutDescription =>
|
||||||
'Un client Flutter open source pour les appareils de réseau mesh MeshCore LoRa.';
|
'Un client Flutter open source pour les appareils de réseau mesh MeshCore LoRa.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_aboutOpenMeteoAttribution =>
|
||||||
|
'Données d\'élévation LOS : Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_infoName => 'Nom';
|
String get settings_infoName => 'Nom';
|
||||||
|
|
||||||
@@ -460,6 +566,14 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageUk => 'Ukrainien';
|
String get appSettings_languageUk => 'Ukrainien';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracing =>
|
||||||
|
'Activer le traçage des messages';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracingSubtitle =>
|
||||||
|
'Afficher les métadonnées détaillées de routage et de synchronisation des messages';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Notifications';
|
String get appSettings_notifications => 'Notifications';
|
||||||
|
|
||||||
@@ -622,6 +736,15 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_offlineMapCache => 'Cache de Carte Hors Ligne';
|
String get appSettings_offlineMapCache => 'Cache de Carte Hors Ligne';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsTitle => 'Unités';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsMetric => 'Métrique (m/km)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsImperial => 'Impérial (ft / mi)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_noAreaSelected => 'Aucune zone sélectionnée';
|
String get appSettings_noAreaSelected => 'Aucune zone sélectionnée';
|
||||||
|
|
||||||
@@ -660,7 +783,35 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
'Les contacts apparaîtront lorsque les appareils font leur annonce.';
|
'Les contacts apparaîtront lorsque les appareils font leur annonce.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_searchContacts => 'Rechercher des contacts...';
|
String get contacts_unread => 'Non lu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_searchContactsNoNumber => 'Rechercher des contacts...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchContacts(int number, String str) {
|
||||||
|
return 'Rechercher des contacts...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchFavorites(int number, String str) {
|
||||||
|
return 'Rechercher $number$str Favoris...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchUsers(int number, String str) {
|
||||||
|
return 'Rechercher $number$str utilisateurs...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRepeaters(int number, String str) {
|
||||||
|
return 'Rechercher $number$str Répéteurs...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRoomServers(int number, String str) {
|
||||||
|
return 'Rechercher $number$str serveurs de salle...';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_noUnreadContacts => 'Aucun contact non lu';
|
String get contacts_noUnreadContacts => 'Aucun contact non lu';
|
||||||
@@ -683,7 +834,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get contacts_manageRoom => 'Gérer le Room Server';
|
String get contacts_manageRoom => 'Gérer le Room Server';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_roomLogin => 'Connexion Salle';
|
String get contacts_roomLogin => 'Connexion Room Server';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_openChat => 'Ouverture du Chat';
|
String get contacts_openChat => 'Ouverture du Chat';
|
||||||
@@ -728,23 +879,23 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_lastSeenMinsAgo(int minutes) {
|
String contacts_lastSeenMinsAgo(int minutes) {
|
||||||
return 'Vu il y a $minutes minutes';
|
return '~ $minutes min.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_lastSeenHourAgo => 'Vu il y a 1 heure';
|
String get contacts_lastSeenHourAgo => '~ 1 heure';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_lastSeenHoursAgo(int hours) {
|
String contacts_lastSeenHoursAgo(int hours) {
|
||||||
return 'Vu il y a $hours heures';
|
return '~ $hours heures';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_lastSeenDayAgo => 'Vu il y a 1 jour';
|
String get contacts_lastSeenDayAgo => '~ 1 jour';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_lastSeenDaysAgo(int days) {
|
String contacts_lastSeenDaysAgo(int days) {
|
||||||
return 'Vu il y a $days jours';
|
return '~ $days jours';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -785,6 +936,12 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_editChannel => 'Modifier le canal';
|
String get channels_editChannel => 'Modifier le canal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_muteChannel => 'Désactiver les notifications du canal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_unmuteChannel => 'Réactiver les notifications du canal';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_deleteChannel => 'Supprimer le canal';
|
String get channels_deleteChannel => 'Supprimer le canal';
|
||||||
|
|
||||||
@@ -793,6 +950,11 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
return 'Supprimer $name? Cela ne peut pas être annulé.';
|
return 'Supprimer $name? Cela ne peut pas être annulé.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channels_channelDeleteFailed(String name) {
|
||||||
|
return 'Échec de la suppression de la chaîne \"$name\"';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String channels_channelDeleted(String name) {
|
String channels_channelDeleted(String name) {
|
||||||
return 'Le canal \"$name\" a été supprimé';
|
return 'Le canal \"$name\" a été supprimé';
|
||||||
@@ -1082,6 +1244,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_pathManagement => 'Gestion des chemins';
|
String get chat_pathManagement => 'Gestion des chemins';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_ShowAllPaths => 'Afficher tous les chemins';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_routingMode => 'Mode de routage';
|
String get chat_routingMode => 'Mode de routage';
|
||||||
|
|
||||||
@@ -1243,6 +1408,12 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_title => 'Carte des nœuds';
|
String get map_title => 'Carte des nœuds';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_lineOfSight => 'Ligne de vue';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_losScreenTitle => 'Ligne de vue';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_noNodesWithLocation =>
|
String get map_noNodesWithLocation =>
|
||||||
'Aucun nœud avec des données de localisation';
|
'Aucun nœud avec des données de localisation';
|
||||||
@@ -1361,6 +1532,13 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_showSharedMarkers => 'Afficher les marqueurs partagés';
|
String get map_showSharedMarkers => 'Afficher les marqueurs partagés';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showGuessedLocations =>
|
||||||
|
'Afficher les emplacements des nœuds estimés';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_guessedLocation => 'Lieu deviné';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_lastSeenTime => 'Dernière fois vu';
|
String get map_lastSeenTime => 'Dernière fois vu';
|
||||||
|
|
||||||
@@ -1531,7 +1709,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get login_repeaterLogin => 'Connexion au répéteur';
|
String get login_repeaterLogin => 'Connexion au répéteur';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get login_roomLogin => 'Connexion Salle';
|
String get login_roomLogin => 'Connexion Room Server';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get login_password => 'Mot de passe';
|
String get login_password => 'Mot de passe';
|
||||||
@@ -1656,7 +1834,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get repeater_management => 'Gestion des répéteurs';
|
String get repeater_management => 'Gestion des répéteurs';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get room_management => 'Administración del Servidor de Habitación';
|
String get room_management => 'Administrattion Room Server';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_managementTools => 'Outils de Gestion';
|
String get repeater_managementTools => 'Outils de Gestion';
|
||||||
@@ -1682,11 +1860,10 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Envoyer des commandes au répéteur';
|
String get repeater_cliSubtitle => 'Envoyer des commandes au répéteur';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Voisins';
|
String get repeater_neighbors => 'Voisins';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle =>
|
String get repeater_neighborsSubtitle => 'Afficher les voisins de saut nuls.';
|
||||||
'Afficher les voisins de saut nuls.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => 'Paramètres';
|
String get repeater_settings => 'Paramètres';
|
||||||
@@ -2391,7 +2568,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Répéteurs Voisins';
|
String get neighbors_repeatersNeighbors => 'Répéteurs Voisins';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData =>
|
String get neighbors_noData =>
|
||||||
@@ -2702,6 +2879,15 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get listFilter_all => 'Tout';
|
String get listFilter_all => 'Tout';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_favorites => 'Préférences';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_addToFavorites => 'Ajouter à mes favoris';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_removeFromFavorites => 'Supprimer des favoris';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_users => 'Utilisateurs';
|
String get listFilter_users => 'Utilisateurs';
|
||||||
|
|
||||||
@@ -2736,6 +2922,145 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get pathTrace_clearTooltip => 'Effacer le chemin';
|
String get pathTrace_clearTooltip => 'Effacer le chemin';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectStartEnd =>
|
||||||
|
'Sélectionnez les nœuds de début et de fin pour LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losRunFailed(String error) {
|
||||||
|
return 'Échec de la vérification de la ligne de vue : $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losClearAllPoints => 'Effacer tous les points';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRunToViewElevationProfile =>
|
||||||
|
'Exécutez LOS pour afficher le profil d\'altitude';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuTitle => 'Menu LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuSubtitle =>
|
||||||
|
'Appuyez sur les nœuds ou appuyez longuement sur la carte pour des points personnalisés';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowDisplayNodes => 'Afficher les nœuds d\'affichage';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losCustomPoints => 'Points personnalisés';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losCustomPointLabel(int index) {
|
||||||
|
return 'Personnalisé $index';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointA => 'Point A';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointB => 'Point B';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaA(String value, String unit) {
|
||||||
|
return 'Antenne A : $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaB(String value, String unit) {
|
||||||
|
return 'Antenne B : $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRun => 'Exécuter la LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losNoElevationData => 'Aucune donnée d\'altitude';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileClear(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String clearance,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, LOS clair, clairance minimale $clearance $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileBlocked(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, bloqué par $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusChecking => 'LOS : vérification...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusNoData => 'LOS : aucune donnée';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||||
|
return 'LOS : $clear/$total clair, $blocked bloqué, $unknown inconnu';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorElevationUnavailable =>
|
||||||
|
'Données d\'altitude indisponibles pour un ou plusieurs échantillons.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorInvalidInput =>
|
||||||
|
'Données de points/d\'altitude non valides pour le calcul de la LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRenameCustomPoint => 'Renommer le point personnalisé';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointName => 'Nom du point';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowPanelTooltip => 'Afficher le panneau LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losHidePanelTooltip => 'Masquer le panneau LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losElevationAttribution =>
|
||||||
|
'Données d’altitude : Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendRadioHorizon => 'Horizon radio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendLosBeam => 'Ligne de visée';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendTerrain => 'Terrain';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyLabel => 'Fréquence';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyInfoTooltip => 'Voir les détails du calcul';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyDialogTitle => 'Calcul de l’horizon radio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losFrequencyDialogDescription(
|
||||||
|
double baselineK,
|
||||||
|
double baselineFreq,
|
||||||
|
double frequencyMHz,
|
||||||
|
double kFactor,
|
||||||
|
) {
|
||||||
|
return 'À partir de k=$baselineK à $baselineFreq MHz, le calcul ajuste le facteur k pour la bande actuelle de $frequencyMHz MHz, ce qui définit la limite incurvée de l\'horizon radio.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_pathTrace => 'Traçage de chemin';
|
String get contacts_pathTrace => 'Traçage de chemin';
|
||||||
|
|
||||||
@@ -2913,4 +3238,90 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open exporter les données de carte GPX';
|
'meshcore-open exporter les données de carte GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Répéteurs à proximité';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Dernière fois vu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_title => 'Paramètres des contacts';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddTitle => 'Découverte automatique';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_otherTitle =>
|
||||||
|
'Autres paramètres liés aux contacts';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersTitle =>
|
||||||
|
'Ajouter automatiquement les utilisateurs';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersSubtitle =>
|
||||||
|
'Autoriser le compagnon à ajouter automatiquement les utilisateurs découverts';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersTitle =>
|
||||||
|
'Ajouter automatiquement les répéteurs';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||||
|
'Autoriser le compagnon à ajouter automatiquement les répéteurs découverts';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersTitle =>
|
||||||
|
'Ajouter automatiquement les serveurs de salle';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||||
|
'Autoriser le compagnon à ajouter automatiquement les serveurs de salles découverts';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsTitle =>
|
||||||
|
'Ajouter automatiquement les capteurs';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||||
|
'Autoriser le compagnon à ajouter automatiquement les capteurs découverts.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestTitle => 'Écraser le plus ancien';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestSubtitle =>
|
||||||
|
'Lorsque la liste de contacts est pleine, le contact le plus ancien non favori sera remplacé.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_Title => 'Contacts découverts';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_noMatching => 'Aucun contact correspondant';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_searchHint =>
|
||||||
|
'Rechercher des contacts découverts';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_contactAdded => 'Contact ajouté';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_addContact => 'Ajouter un contact';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_copyContact =>
|
||||||
|
'Copier le contact dans le presse-papiers';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContact => 'Supprimer le contact';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAll =>
|
||||||
|
'Supprimer tous les contacts découverts';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAllContent =>
|
||||||
|
'Êtes-vous sûr de vouloir supprimer tous les contacts découverts ?';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get common_delete => 'Elimina';
|
String get common_delete => 'Elimina';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get common_deleteAll => 'Elimina tutto';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get common_close => 'Chiudi';
|
String get common_close => 'Chiudi';
|
||||||
|
|
||||||
@@ -108,6 +111,92 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
String get scanner_title => 'MeshCore Open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceUsbLabel => 'USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Connessione tramite USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Seleziona il dispositivo seriale rilevato e connettilo direttamente al tuo nodo MeshCore.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Seleziona un dispositivo USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'Nessun dispositivo USB rilevato. Collegare uno e aggiornare.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied =>
|
||||||
|
'È stato negato l\'accesso tramite USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing =>
|
||||||
|
'Il dispositivo USB selezionato non è più disponibile.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Seleziona un dispositivo USB valido.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy =>
|
||||||
|
'Un\'altra richiesta di connessione tramite USB è già in corso.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'Non è collegato alcun dispositivo USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed =>
|
||||||
|
'Impossibile aprire il dispositivo USB selezionato.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'Impossibile connettersi al dispositivo USB selezionato.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'La comunicazione seriale tramite USB non è supportata su questa piattaforma.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'La connessione USB è già attiva.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected =>
|
||||||
|
'Non è stato selezionato alcun dispositivo USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'La connessione USB non è attiva.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName =>
|
||||||
|
'Dispositivo per comunicazione seriale su rete';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Seleziona un dispositivo USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Connessione al dispositivo USB...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Ricerca di dispositivi USB...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'Errore nella connessione USB: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Scansione in corso per i dispositivi...';
|
String get scanner_scanning => 'Scansione in corso per i dispositivi...';
|
||||||
|
|
||||||
@@ -150,6 +239,13 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
String get scanner_bluetoothOffMessage =>
|
String get scanner_bluetoothOffMessage =>
|
||||||
'Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.';
|
'Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequired => 'Browser Chrome richiesto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequiredMessage =>
|
||||||
|
'Questa applicazione web richiede Google Chrome o un browser basato su Chromium per il supporto Bluetooth.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_enableBluetooth => 'Abilita il Bluetooth';
|
String get scanner_enableBluetooth => 'Abilita il Bluetooth';
|
||||||
|
|
||||||
@@ -233,6 +329,13 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_longitude => 'Longitudine';
|
String get settings_longitude => 'Longitudine';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettings => 'Impostazioni di contatto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettingsSubtitle =>
|
||||||
|
'Impostazioni per l\'aggiunta dei contatti';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_privacyMode => 'Modalità Privacy';
|
String get settings_privacyMode => 'Modalità Privacy';
|
||||||
|
|
||||||
@@ -322,6 +425,10 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
String get settings_aboutDescription =>
|
String get settings_aboutDescription =>
|
||||||
'Un client Flutter open-source per i dispositivi di rete mesh LoRa Core di MeshCore.';
|
'Un client Flutter open-source per i dispositivi di rete mesh LoRa Core di MeshCore.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_aboutOpenMeteoAttribution =>
|
||||||
|
'Dati di elevazione LOS: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_infoName => 'Nome';
|
String get settings_infoName => 'Nome';
|
||||||
|
|
||||||
@@ -458,6 +565,14 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageUk => 'Ucraino';
|
String get appSettings_languageUk => 'Ucraino';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracing =>
|
||||||
|
'Abilita tracciamento messaggi';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracingSubtitle =>
|
||||||
|
'Mostra metadati dettagliati su instradamento e tempi per i messaggi';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Notifiche';
|
String get appSettings_notifications => 'Notifiche';
|
||||||
|
|
||||||
@@ -619,6 +734,15 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_offlineMapCache => 'Cache Mappa Offline';
|
String get appSettings_offlineMapCache => 'Cache Mappa Offline';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsTitle => 'Unità';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsMetric => 'Metrico (m/km)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsImperial => 'Imperiale (ft / mi)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_noAreaSelected => 'Nessun\'area selezionata';
|
String get appSettings_noAreaSelected => 'Nessun\'area selezionata';
|
||||||
|
|
||||||
@@ -656,7 +780,35 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
'I contatti appariranno quando i dispositivi pubblicizzano.';
|
'I contatti appariranno quando i dispositivi pubblicizzano.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_searchContacts => 'Cerca contatti...';
|
String get contacts_unread => 'Non letti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_searchContactsNoNumber => 'Cerca Contatti...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchContacts(int number, String str) {
|
||||||
|
return 'Cerca contatti...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchFavorites(int number, String str) {
|
||||||
|
return 'Cerca $number$str Preferiti...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchUsers(int number, String str) {
|
||||||
|
return 'Cerca $number$str Utenti...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRepeaters(int number, String str) {
|
||||||
|
return 'Cerca $number$str Ripetitori...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRoomServers(int number, String str) {
|
||||||
|
return 'Cerca $number$str server Room...';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_noUnreadContacts => 'Nessun contatto non letto';
|
String get contacts_noUnreadContacts => 'Nessun contatto non letto';
|
||||||
@@ -781,6 +933,12 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_editChannel => 'Modifica canale';
|
String get channels_editChannel => 'Modifica canale';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_muteChannel => 'Silenzia canale';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_unmuteChannel => 'Attiva notifiche canale';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_deleteChannel => 'Elimina canale';
|
String get channels_deleteChannel => 'Elimina canale';
|
||||||
|
|
||||||
@@ -789,6 +947,11 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
return 'Eliminare \"$name\"? Non può essere annullato.';
|
return 'Eliminare \"$name\"? Non può essere annullato.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channels_channelDeleteFailed(String name) {
|
||||||
|
return 'Impossibile eliminare il canale \"$name\"';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String channels_channelDeleted(String name) {
|
String channels_channelDeleted(String name) {
|
||||||
return 'Canale \"$name\" eliminato';
|
return 'Canale \"$name\" eliminato';
|
||||||
@@ -1077,6 +1240,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_pathManagement => 'Gestione Percorsi';
|
String get chat_pathManagement => 'Gestione Percorsi';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_ShowAllPaths => 'Mostra tutti i percorsi';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_routingMode => 'Modalità di routing';
|
String get chat_routingMode => 'Modalità di routing';
|
||||||
|
|
||||||
@@ -1236,6 +1402,12 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_title => 'Mappa Nodi';
|
String get map_title => 'Mappa Nodi';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_lineOfSight => 'Linea di vista';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_losScreenTitle => 'Linea di vista';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_noNodesWithLocation => 'Nessun nodo con dati di posizione';
|
String get map_noNodesWithLocation => 'Nessun nodo con dati di posizione';
|
||||||
|
|
||||||
@@ -1353,6 +1525,12 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_showSharedMarkers => 'Mostra i segnaposto condivisi';
|
String get map_showSharedMarkers => 'Mostra i segnaposto condivisi';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showGuessedLocations => 'Mostra le posizioni stimate dei nodi';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_guessedLocation => 'Località indovinata';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_lastSeenTime => 'Ultimo Tempo di Visualizzazione';
|
String get map_lastSeenTime => 'Ultimo Tempo di Visualizzazione';
|
||||||
|
|
||||||
@@ -1672,10 +1850,10 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Invia comandi al ripetitore';
|
String get repeater_cliSubtitle => 'Invia comandi al ripetitore';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Vicini';
|
String get repeater_neighbors => 'Vicini';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle =>
|
String get repeater_neighborsSubtitle =>
|
||||||
'Visualizza vicini di salto pari a zero.';
|
'Visualizza vicini di salto pari a zero.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -2376,7 +2554,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Ripetitori Vicini';
|
String get neighbors_repeatersNeighbors => 'Ripetitori Vicini';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Nessun dato sugli vicini disponibile.';
|
String get neighbors_noData => 'Nessun dato sugli vicini disponibile.';
|
||||||
@@ -2685,6 +2863,15 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get listFilter_all => 'Tutti';
|
String get listFilter_all => 'Tutti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_favorites => 'Preferiti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_addToFavorites => 'Aggiungi ai preferiti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_removeFromFavorites => 'Rimuovi dai preferiti';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_users => 'Utenti';
|
String get listFilter_users => 'Utenti';
|
||||||
|
|
||||||
@@ -2720,6 +2907,145 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get pathTrace_clearTooltip => 'Pulisci percorso';
|
String get pathTrace_clearTooltip => 'Pulisci percorso';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectStartEnd =>
|
||||||
|
'Seleziona i nodi iniziali e finali per la LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losRunFailed(String error) {
|
||||||
|
return 'Controllo della linea di vista fallito: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losClearAllPoints => 'Cancella tutti i punti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRunToViewElevationProfile =>
|
||||||
|
'Eseguire LOS per visualizzare il profilo altimetrico';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuTitle => 'Menù LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuSubtitle =>
|
||||||
|
'Tocca i nodi o premi a lungo la mappa per punti personalizzati';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowDisplayNodes => 'Mostra i nodi di visualizzazione';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losCustomPoints => 'Punti personalizzati';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losCustomPointLabel(int index) {
|
||||||
|
return 'Personalizzato $index';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointA => 'Punto A';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointB => 'Punto B';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaA(String value, String unit) {
|
||||||
|
return 'Antenna A: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaB(String value, String unit) {
|
||||||
|
return 'Antenna B: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRun => 'Esegui LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losNoElevationData => 'Nessun dato di elevazione';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileClear(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String clearance,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, libera LOS, distanza minima $clearance $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileBlocked(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, bloccato da $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusChecking => 'LOS: controllo...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusNoData => 'LOS: nessun dato';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||||
|
return 'LOS: $clear/$total libera, $blocked bloccato, $unknown sconosciuto';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorElevationUnavailable =>
|
||||||
|
'Dati di elevazione non disponibili per uno o più campioni.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorInvalidInput =>
|
||||||
|
'Dati punti/elevazione non validi per il calcolo della LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRenameCustomPoint => 'Rinomina punto personalizzato';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointName => 'Nome del punto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowPanelTooltip => 'Mostra il pannello LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losHidePanelTooltip => 'Nascondi il pannello LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losElevationAttribution =>
|
||||||
|
'Dati di elevazione: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendRadioHorizon => 'Orizzonte radio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendLosBeam => 'Linea di vista';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendTerrain => 'Terreno';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyLabel => 'Frequenza';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyInfoTooltip => 'Visualizza i dettagli del calcolo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyDialogTitle => 'Calcolo dell’orizzonte radio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losFrequencyDialogDescription(
|
||||||
|
double baselineK,
|
||||||
|
double baselineFreq,
|
||||||
|
double frequencyMHz,
|
||||||
|
double kFactor,
|
||||||
|
) {
|
||||||
|
return 'Partendo da k=$baselineK a $baselineFreq MHz, il calcolo regola il fattore k per l\'attuale banda $frequencyMHz MHz, che definisce il limite curvo dell\'orizzonte radio.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_pathTrace => 'Traccia Percorso';
|
String get contacts_pathTrace => 'Traccia Percorso';
|
||||||
|
|
||||||
@@ -2893,4 +3219,89 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open esportazione dati mappa GPX';
|
'meshcore-open esportazione dati mappa GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Ripetitori vicini';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Ultimo accesso';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_title => 'Impostazioni dei contatti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddTitle => 'Scoperta automatica';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_otherTitle =>
|
||||||
|
'Altre impostazioni relative ai contatti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersTitle =>
|
||||||
|
'Aggiungere utenti automaticamente';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersSubtitle =>
|
||||||
|
'Consenti al compagno di aggiungere automaticamente gli utenti scoperti.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersTitle =>
|
||||||
|
'Aggiungere ripetitori automaticamente';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||||
|
'Consenti al compagno di aggiungere automaticamente i ripetitori scoperti.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersTitle =>
|
||||||
|
'Aggiungere automaticamente i server delle stanze';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||||
|
'Consenti al compagno di aggiungere automaticamente i server delle stanze scoperte.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsTitle =>
|
||||||
|
'Aggiungere automaticamente i sensori';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||||
|
'Consenti al compagno di aggiungere automaticamente i sensori scoperti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestTitle =>
|
||||||
|
'Sostituisci il più vecchio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestSubtitle =>
|
||||||
|
'Quando l\'elenco dei contatti è pieno, il contatto più vecchio non tra i preferiti verrà sostituito.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_Title => 'Contatti scoperti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_noMatching => 'Nessun contatto corrispondente';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_searchHint => 'Cerca contatti scoperti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_contactAdded => 'Contatto aggiunto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_addContact => 'Aggiungi contatto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_copyContact => 'Copia contatto negli appunti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContact => 'Elimina Contatto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAll =>
|
||||||
|
'Eliminare tutti i contatti scoperti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAllContent =>
|
||||||
|
'Sei sicuro di voler eliminare tutti i contatti scoperti?';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get common_delete => 'Verwijderen';
|
String get common_delete => 'Verwijderen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get common_deleteAll => 'Alles verwijderen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get common_close => 'Sluiten';
|
String get common_close => 'Sluiten';
|
||||||
|
|
||||||
@@ -108,6 +111,89 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
String get scanner_title => 'MeshCore Open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceUsbLabel => 'USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Verbind via USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Selecteer een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Selecteer een USB-apparaat';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'USB-serieel is actief op ondersteunde Android-apparaten en desktop-platforms.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'Geen USB-apparaten gevonden. Sluit er een aan en herlaad.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied => 'Toegang via USB is geweigerd.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing =>
|
||||||
|
'Het geselecteerde USB-apparaat is niet meer beschikbaar.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Selecteer een geldig USB-apparaat.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy =>
|
||||||
|
'Een andere verzoek om een USB-verbinding is al in behandeling.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'Er is geen USB-apparaat aangesloten.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed =>
|
||||||
|
'Kon het geselecteerde USB-apparaat niet openen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'Kon niet verbinding maken met het geselecteerde USB-apparaat.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'USB-serieel is niet ondersteund op deze platform.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'Een USB-verbinding is al actief.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected => 'Geen USB-apparaat is geselecteerd.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'De USB-verbinding is niet actief.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName => 'Web-serieapparaat';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Selecteer een USB-apparaat';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Verbinding maken met USB-apparaat...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Zoeken naar USB-apparaten...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'Fout bij de USB-verbinding: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Scannen naar apparaten...';
|
String get scanner_scanning => 'Scannen naar apparaten...';
|
||||||
|
|
||||||
@@ -149,6 +235,13 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
String get scanner_bluetoothOffMessage =>
|
String get scanner_bluetoothOffMessage =>
|
||||||
'Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.';
|
'Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequired => 'Chrome-browser vereist';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequiredMessage =>
|
||||||
|
'Deze webapplicatie vereist Google Chrome of een op Chromium gebaseerde browser voor Bluetooth-ondersteuning.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_enableBluetooth => 'Activeer Bluetooth';
|
String get scanner_enableBluetooth => 'Activeer Bluetooth';
|
||||||
|
|
||||||
@@ -233,6 +326,13 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_longitude => 'Lengtegraad';
|
String get settings_longitude => 'Lengtegraad';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettings => 'Contactinstellingen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettingsSubtitle =>
|
||||||
|
'Instellingen voor het toevoegen van contacten';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_privacyMode => 'Privacy Mode';
|
String get settings_privacyMode => 'Privacy Mode';
|
||||||
|
|
||||||
@@ -320,6 +420,10 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
String get settings_aboutDescription =>
|
String get settings_aboutDescription =>
|
||||||
'Een open-source Flutter client voor MeshCore LoRa mesh netwerkapparaten.';
|
'Een open-source Flutter client voor MeshCore LoRa mesh netwerkapparaten.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_aboutOpenMeteoAttribution =>
|
||||||
|
'LOS-hoogtegegevens: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_infoName => 'Naam';
|
String get settings_infoName => 'Naam';
|
||||||
|
|
||||||
@@ -456,6 +560,13 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageUk => 'Oekraïens';
|
String get appSettings_languageUk => 'Oekraïens';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracing => 'Berichttracking inschakelen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracingSubtitle =>
|
||||||
|
'Gedetailleerde routerings- en timing-metadata voor berichten weergeven';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Notificaties';
|
String get appSettings_notifications => 'Notificaties';
|
||||||
|
|
||||||
@@ -617,6 +728,15 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_offlineMapCache => 'Offline Kaarten Cache';
|
String get appSettings_offlineMapCache => 'Offline Kaarten Cache';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsTitle => 'Eenheden';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsMetric => 'Metrisch (m / km)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsImperial => 'Imperiaal (ft / mi)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_noAreaSelected => 'Geen gebied geselecteerd';
|
String get appSettings_noAreaSelected => 'Geen gebied geselecteerd';
|
||||||
|
|
||||||
@@ -654,7 +774,35 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
'Contacten verschijnen wanneer apparaten zich aanbieden.';
|
'Contacten verschijnen wanneer apparaten zich aanbieden.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_searchContacts => 'Zoek contacten...';
|
String get contacts_unread => 'Ongelezen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_searchContactsNoNumber => 'Zoek contacten...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchContacts(int number, String str) {
|
||||||
|
return 'Zoek contacten...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchFavorites(int number, String str) {
|
||||||
|
return 'Zoek $number$str favorieten...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchUsers(int number, String str) {
|
||||||
|
return 'Zoek $number$str gebruikers...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRepeaters(int number, String str) {
|
||||||
|
return 'Zoek $number$str Repeaters...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRoomServers(int number, String str) {
|
||||||
|
return 'Zoek $number$str Room servers...';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_noUnreadContacts => 'Geen ongelezen contacten';
|
String get contacts_noUnreadContacts => 'Geen ongelezen contacten';
|
||||||
@@ -779,6 +927,12 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_editChannel => 'Kanaal bewerken';
|
String get channels_editChannel => 'Kanaal bewerken';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_muteChannel => 'Kanaal dempen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_unmuteChannel => 'Kanaal dempen opheffen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_deleteChannel => 'Kanaal verwijderen';
|
String get channels_deleteChannel => 'Kanaal verwijderen';
|
||||||
|
|
||||||
@@ -787,6 +941,11 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
return 'Verwijderen \"$name\"? Dit kan niet worden teruggedraaid.';
|
return 'Verwijderen \"$name\"? Dit kan niet worden teruggedraaid.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channels_channelDeleteFailed(String name) {
|
||||||
|
return 'Kan kanaal $name niet verwijderen';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String channels_channelDeleted(String name) {
|
String channels_channelDeleted(String name) {
|
||||||
return 'Kanaal \"$name\" verwijderd';
|
return 'Kanaal \"$name\" verwijderd';
|
||||||
@@ -1074,6 +1233,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_pathManagement => 'Beheer van Paden';
|
String get chat_pathManagement => 'Beheer van Paden';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_ShowAllPaths => 'Toon alle paden';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_routingMode => 'Routeerwijze';
|
String get chat_routingMode => 'Routeerwijze';
|
||||||
|
|
||||||
@@ -1232,6 +1394,12 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_title => 'Node Map';
|
String get map_title => 'Node Map';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_lineOfSight => 'Zichtlijn';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_losScreenTitle => 'Zichtlijn';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_noNodesWithLocation => 'Geen nodes met locatiegegevens';
|
String get map_noNodesWithLocation => 'Geen nodes met locatiegegevens';
|
||||||
|
|
||||||
@@ -1349,6 +1517,13 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_showSharedMarkers => 'Toon gedeelde markeringen';
|
String get map_showSharedMarkers => 'Toon gedeelde markeringen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showGuessedLocations =>
|
||||||
|
'Toon de voorspelde locaties van de knopen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_guessedLocation => 'Geroerde locatie';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_lastSeenTime => 'Laatste Bekeken Tijd';
|
String get map_lastSeenTime => 'Laatste Bekeken Tijd';
|
||||||
|
|
||||||
@@ -1668,10 +1843,10 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Verzend commando\'s naar de repeater';
|
String get repeater_cliSubtitle => 'Verzend commando\'s naar de repeater';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Buren';
|
String get repeater_neighbors => 'Buren';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle => 'Bekijk nul hops buren.';
|
String get repeater_neighborsSubtitle => 'Bekijk nul hops buren.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => 'Instellingen';
|
String get repeater_settings => 'Instellingen';
|
||||||
@@ -2367,7 +2542,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Herhalingen Buren';
|
String get neighbors_repeatersNeighbors => 'Herhalingen Buren';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Geen gegevens van buren beschikbaar.';
|
String get neighbors_noData => 'Geen gegevens van buren beschikbaar.';
|
||||||
@@ -2677,6 +2852,15 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get listFilter_all => 'Alles';
|
String get listFilter_all => 'Alles';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_favorites => 'Favorieten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_addToFavorites => 'Toevoegen aan favorieten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_removeFromFavorites => 'Verwijderen uit favorieten';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_users => 'Gebruikers';
|
String get listFilter_users => 'Gebruikers';
|
||||||
|
|
||||||
@@ -2711,6 +2895,145 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get pathTrace_clearTooltip => 'Weg wissen';
|
String get pathTrace_clearTooltip => 'Weg wissen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectStartEnd =>
|
||||||
|
'Selecteer begin- en eindknooppunten voor LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losRunFailed(String error) {
|
||||||
|
return 'Zichtlijncontrole mislukt: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losClearAllPoints => 'Wis alle punten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRunToViewElevationProfile =>
|
||||||
|
'Voer LOS uit om het hoogteprofiel te bekijken';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuTitle => 'LOS-menu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuSubtitle =>
|
||||||
|
'Tik op knooppunten of druk lang op de kaart voor aangepaste punten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowDisplayNodes => 'Toon weergaveknooppunten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losCustomPoints => 'Aangepaste punten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losCustomPointLabel(int index) {
|
||||||
|
return 'Aangepast $index';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointA => 'Punt A';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointB => 'Punt B';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaA(String value, String unit) {
|
||||||
|
return 'Antenne A: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaB(String value, String unit) {
|
||||||
|
return 'Antenne B: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRun => 'Voer LOS uit';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losNoElevationData => 'Geen hoogtegegevens';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileClear(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String clearance,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, vrije LOS, min. vrije ruimte $clearance $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileBlocked(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, geblokkeerd door $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusChecking => 'LOS: controleren...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusNoData => 'LOS: geen gegevens';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||||
|
return 'LOS: $clear/$total gewist, $blocked geblokkeerd, $unknown onbekend';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorElevationUnavailable =>
|
||||||
|
'Hoogtegegevens niet beschikbaar voor een of meer monsters.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorInvalidInput =>
|
||||||
|
'Ongeldige punten/hoogtegegevens voor LOS-berekening.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRenameCustomPoint => 'Hernoem aangepast punt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointName => 'Puntnaam';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowPanelTooltip => 'Toon LOS-paneel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losHidePanelTooltip => 'LOS-paneel verbergen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losElevationAttribution =>
|
||||||
|
'Hoogtegegevens: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendRadioHorizon => 'Radiohorizon';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendLosBeam => 'Zichtlijn';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendTerrain => 'Terrein';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyLabel => 'Frequentie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyInfoTooltip => 'Bekijk details van de berekening';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyDialogTitle => 'Berekening van de radiohorizon';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losFrequencyDialogDescription(
|
||||||
|
double baselineK,
|
||||||
|
double baselineFreq,
|
||||||
|
double frequencyMHz,
|
||||||
|
double kFactor,
|
||||||
|
) {
|
||||||
|
return 'Beginnend met k=$baselineK bij $baselineFreq MHz, wordt bij de berekening de k-factor aangepast voor de huidige $frequencyMHz MHz-band, die de gebogen radiohorizonkap definieert.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_pathTrace => 'Pad Traceren';
|
String get contacts_pathTrace => 'Pad Traceren';
|
||||||
|
|
||||||
@@ -2881,4 +3204,88 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open GPX kaartgegevens exporteren';
|
'meshcore-open GPX kaartgegevens exporteren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Nabije herhalingseenheden';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Laatst gezien';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_title => 'Instellingen voor contacten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddTitle => 'Automatische detectie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_otherTitle =>
|
||||||
|
'Andere instellingen voor contactgerelateerde zaken';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersTitle =>
|
||||||
|
'Gebruikers automatisch toevoegen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersSubtitle =>
|
||||||
|
'Sta toe dat de companion automatisch ontdekte gebruikers toevoegt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersTitle =>
|
||||||
|
'Automatisch herhalingstoestellen toevoegen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||||
|
'Sta toe dat de companion automatisch ontdekte repeaters toevoegt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersTitle =>
|
||||||
|
'Automatisch kamerservers toevoegen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||||
|
'Sta toe dat de companion automatisch ontdekte kamer servers toevoegt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsTitle =>
|
||||||
|
'Automatisch sensoren toevoegen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||||
|
'Sta toe dat de companion automatisch ontdekte sensoren toevoegt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestTitle => 'Overschrijf Oudste';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestSubtitle =>
|
||||||
|
'Wanneer de contactenlijst vol is, wordt de oudste niet-favoriete contactpersoon vervangen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_Title => 'Ontdekte contacten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_noMatching => 'Geen overeenkomende contacten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_searchHint => 'Ontdekte contacten zoeken';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_contactAdded => 'Contact toegevoegd';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_addContact => 'Contact toevoegen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_copyContact => 'Kopieer contact naar klembord';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContact => 'Contact verwijderen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAll =>
|
||||||
|
'Verwijder alle ontdekte contacten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAllContent =>
|
||||||
|
'Weet u zeker dat u alle ontdekte contacten wilt verwijderen?';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get common_delete => 'Usuń';
|
String get common_delete => 'Usuń';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get common_deleteAll => 'Usuń wszystko';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get common_close => 'Zamknąć';
|
String get common_close => 'Zamknąć';
|
||||||
|
|
||||||
@@ -108,6 +111,92 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
String get scanner_title => 'MeshCore Open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceUsbLabel => 'USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Połącz przez USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Wybierz wykryty urządzenie szeregowe i podłącz je bezpośrednio do swojego węzła MeshCore.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Wybierz urządzenie USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'Port szeregowy USB jest aktywny na urządzeniach z systemem Android i platformach stacjonarnych, które go obsługują.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'Nie znaleziono żadnych urządzeń USB. Podłącz jedno i zaktualizuj.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied =>
|
||||||
|
'Zostało odrzucone żądanie dostępu przez USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing =>
|
||||||
|
'Wybór urządzenia USB już nie jest dostępny.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Wybierz prawidłowe urządzenie USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy =>
|
||||||
|
'Kolejne żądanie połączenia przez USB jest już w trakcie realizacji.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'Brak podłączonego urządzenia USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed =>
|
||||||
|
'Nie udało się otworzyć wybranego urządzenia USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'Nie udało się połączyć z wybranym urządzeniem USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'Port szeregowy USB nie jest obsługiwany na tym urządzeniu.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'Połączenie USB jest już aktywne.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected =>
|
||||||
|
'Nie został wybrany żaden urządzenie USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'Połączenie USB nie jest aktywne.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'Połączenie nie zostało nawiązane. Upewnij się, że urządzenie posiada oprogramowanie \"USB Companion\".';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName =>
|
||||||
|
'Urządzenie do komunikacji przez sieć (seria)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Wybierz urządzenie USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Połączenie z urządzeniem USB...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Wyszukiwanie urządzeń USB...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'Błąd połączenia USB: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Skanowanie urządzeń...';
|
String get scanner_scanning => 'Skanowanie urządzeń...';
|
||||||
|
|
||||||
@@ -150,6 +239,13 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
String get scanner_bluetoothOffMessage =>
|
String get scanner_bluetoothOffMessage =>
|
||||||
'Prosimy włączyć Bluetooth, aby przeskanować urządzenia.';
|
'Prosimy włączyć Bluetooth, aby przeskanować urządzenia.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequired => 'Wymagana przeglądarka Chrome';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequiredMessage =>
|
||||||
|
'Ta aplikacja internetowa wymaga przeglądarki Google Chrome lub opartej na Chromium do obsługi Bluetooth.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_enableBluetooth => 'Włącz Bluetooth';
|
String get scanner_enableBluetooth => 'Włącz Bluetooth';
|
||||||
|
|
||||||
@@ -235,6 +331,13 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_longitude => 'Długość';
|
String get settings_longitude => 'Długość';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettings => 'Ustawienia kontaktowe';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettingsSubtitle =>
|
||||||
|
'Ustawienia dotyczące sposobu dodawania kontaktów';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_privacyMode => 'Tryb Prywatny';
|
String get settings_privacyMode => 'Tryb Prywatny';
|
||||||
|
|
||||||
@@ -323,6 +426,10 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
String get settings_aboutDescription =>
|
String get settings_aboutDescription =>
|
||||||
'Otwarty kod źródłowy klient Flutter dla urządzeń do sieci mesh LoRa MeshCore.';
|
'Otwarty kod źródłowy klient Flutter dla urządzeń do sieci mesh LoRa MeshCore.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_aboutOpenMeteoAttribution =>
|
||||||
|
'Dane wysokościowe LOS: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_infoName => 'Imię';
|
String get settings_infoName => 'Imię';
|
||||||
|
|
||||||
@@ -460,6 +567,13 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageUk => 'Ukraińska';
|
String get appSettings_languageUk => 'Ukraińska';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracing => 'Włącz śledzenie wiadomości';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracingSubtitle =>
|
||||||
|
'Pokaż szczegółowe metadane trasowania i czasu dla wiadomości';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Powiadomienia';
|
String get appSettings_notifications => 'Powiadomienia';
|
||||||
|
|
||||||
@@ -621,6 +735,15 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_offlineMapCache => 'Bufor Map Offline';
|
String get appSettings_offlineMapCache => 'Bufor Map Offline';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsTitle => 'Jednostki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsMetric => 'Metryczne (m / km)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsImperial => 'Imperialne (ft / mi)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_noAreaSelected => 'Nie zaznaczono żadnej powierzchni.';
|
String get appSettings_noAreaSelected => 'Nie zaznaczono żadnej powierzchni.';
|
||||||
|
|
||||||
@@ -658,7 +781,35 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
'Kontakty będą wyświetlane, gdy urządzenia reklamują się.';
|
'Kontakty będą wyświetlane, gdy urządzenia reklamują się.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_searchContacts => 'Wyszukaj kontakty...';
|
String get contacts_unread => 'Nieprzeczytane';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_searchContactsNoNumber => 'Wyszukaj kontakty...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchContacts(int number, String str) {
|
||||||
|
return 'Wyszukaj kontakty...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchFavorites(int number, String str) {
|
||||||
|
return 'Wyszukaj $number$str ulubione...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchUsers(int number, String str) {
|
||||||
|
return 'Wyszukaj $number$str Użytkowników...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRepeaters(int number, String str) {
|
||||||
|
return 'Wyszukaj $number$str powtórników...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRoomServers(int number, String str) {
|
||||||
|
return 'Wyszukaj $number$str serwerów Room...';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_noUnreadContacts => 'Brak nieprzeczytanych kontaktów';
|
String get contacts_noUnreadContacts => 'Brak nieprzeczytanych kontaktów';
|
||||||
@@ -784,6 +935,12 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_editChannel => 'Edytuj kanał';
|
String get channels_editChannel => 'Edytuj kanał';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_muteChannel => 'Wycisz kanał';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_unmuteChannel => 'Wyłącz wyciszenie kanału';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_deleteChannel => 'Usuń kanał';
|
String get channels_deleteChannel => 'Usuń kanał';
|
||||||
|
|
||||||
@@ -792,6 +949,11 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
return 'Usuń \"$name\"? Nie można tego cofnąć.';
|
return 'Usuń \"$name\"? Nie można tego cofnąć.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channels_channelDeleteFailed(String name) {
|
||||||
|
return 'Nie udało się usunąć kanału \"$name\"';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String channels_channelDeleted(String name) {
|
String channels_channelDeleted(String name) {
|
||||||
return 'Kanał \"$name\" usunięto';
|
return 'Kanał \"$name\" usunięto';
|
||||||
@@ -1079,6 +1241,9 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_pathManagement => 'Zarządzanie ścieżkami';
|
String get chat_pathManagement => 'Zarządzanie ścieżkami';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_ShowAllPaths => 'Pokaż wszystkie ścieżki';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_routingMode => 'Tryb routingu';
|
String get chat_routingMode => 'Tryb routingu';
|
||||||
|
|
||||||
@@ -1238,6 +1403,12 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_title => 'Mapa węzłów';
|
String get map_title => 'Mapa węzłów';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_lineOfSight => 'Linia wzroku';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_losScreenTitle => 'Linia wzroku';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_noNodesWithLocation => 'Brak węzłów z danymi lokalizacyjnymi';
|
String get map_noNodesWithLocation => 'Brak węzłów z danymi lokalizacyjnymi';
|
||||||
|
|
||||||
@@ -1355,6 +1526,13 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_showSharedMarkers => 'Pokaż współdzielone znaki.';
|
String get map_showSharedMarkers => 'Pokaż współdzielone znaki.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showGuessedLocations =>
|
||||||
|
'Wyświetl lokalizacje zgadanych węzłów';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_guessedLocation => 'Wydana lokalizacja';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_lastSeenTime => 'Ostatni raz widiany';
|
String get map_lastSeenTime => 'Ostatni raz widiany';
|
||||||
|
|
||||||
@@ -1676,10 +1854,10 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Wyślij polecenia do powielacza';
|
String get repeater_cliSubtitle => 'Wyślij polecenia do powielacza';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Sąsiedzi';
|
String get repeater_neighbors => 'Sąsiedzi';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle =>
|
String get repeater_neighborsSubtitle =>
|
||||||
'Wyświetl sąsiedztwo zerowych hopów.';
|
'Wyświetl sąsiedztwo zerowych hopów.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -2375,7 +2553,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Powtarzacze Sąsiedzi';
|
String get neighbors_repeatersNeighbors => 'Powtarzacze Sąsiedzi';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Brak danych dotyczących sąsiadów.';
|
String get neighbors_noData => 'Brak danych dotyczących sąsiadów.';
|
||||||
@@ -2684,6 +2862,15 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get listFilter_all => 'Wszystko';
|
String get listFilter_all => 'Wszystko';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_favorites => 'Ulubione';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_addToFavorites => 'Dodaj do ulubionych';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_removeFromFavorites => 'Usuń z ulubionych';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_users => 'Użytkownicy';
|
String get listFilter_users => 'Użytkownicy';
|
||||||
|
|
||||||
@@ -2718,6 +2905,144 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get pathTrace_clearTooltip => 'Wyczyść ścieżkę';
|
String get pathTrace_clearTooltip => 'Wyczyść ścieżkę';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectStartEnd => 'Wybierz węzły początkowe i końcowe dla LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losRunFailed(String error) {
|
||||||
|
return 'Sprawdzenie pola widzenia nie powiodło się: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losClearAllPoints => 'Wyczyść wszystkie punkty';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRunToViewElevationProfile =>
|
||||||
|
'Uruchom LOS, aby wyświetlić profil wysokości';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuTitle => 'Menu LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuSubtitle =>
|
||||||
|
'Stuknij węzły lub naciśnij i przytrzymaj mapę, aby uzyskać niestandardowe punkty';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowDisplayNodes => 'Pokaż węzły wyświetlające';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losCustomPoints => 'Punkty niestandardowe';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losCustomPointLabel(int index) {
|
||||||
|
return 'Niestandardowe $index';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointA => 'Punkt A';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointB => 'Punkt B';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaA(String value, String unit) {
|
||||||
|
return 'Antena A: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaB(String value, String unit) {
|
||||||
|
return 'Antena B: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRun => 'Uruchom LOS-a';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losNoElevationData => 'Brak danych o wysokości';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileClear(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String clearance,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, czysty LOS, minimalny prześwit $clearance $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileBlocked(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, zablokowane przez $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusChecking => 'LOS: sprawdzam...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusNoData => 'LOS: brak danych';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||||
|
return 'LOS: $clear/$total jasne, $blocked zablokowane, $unknown nieznane';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorElevationUnavailable =>
|
||||||
|
'Dane dotyczące wysokości są niedostępne dla jednej lub większej liczby próbek.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorInvalidInput =>
|
||||||
|
'Nieprawidłowe dane punktów/wysokości do obliczenia LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRenameCustomPoint => 'Zmień nazwę punktu niestandardowego';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointName => 'Nazwa punktu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowPanelTooltip => 'Pokaż panel LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losHidePanelTooltip => 'Ukryj panel LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losElevationAttribution =>
|
||||||
|
'Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendRadioHorizon => 'Horyzont radiowy';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendLosBeam => 'Linia widoczności';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendTerrain => 'Teren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyLabel => 'Częstotliwość';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyInfoTooltip => 'Zobacz szczegóły obliczenia';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyDialogTitle => 'Obliczanie horyzontu radiowego';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losFrequencyDialogDescription(
|
||||||
|
double baselineK,
|
||||||
|
double baselineFreq,
|
||||||
|
double frequencyMHz,
|
||||||
|
double kFactor,
|
||||||
|
) {
|
||||||
|
return 'Zaczynając od k=$baselineK przy $baselineFreq MHz, obliczenia korygują współczynnik k dla bieżącego pasma $frequencyMHz MHz, które definiuje zakrzywiony limit horyzontu radiowego.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_pathTrace => 'Śledzenie Ścieżek';
|
String get contacts_pathTrace => 'Śledzenie Ścieżek';
|
||||||
|
|
||||||
@@ -2895,4 +3220,88 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'Eksport danych mapy GPX meshcore-open';
|
'Eksport danych mapy GPX meshcore-open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Nadajniki w pobliżu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Ostatnio widziany';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_title => 'Ustawienia kontaktów';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddTitle => 'Automatyczne odnajdywanie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_otherTitle =>
|
||||||
|
'Inne ustawienia związane z kontaktami';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersTitle =>
|
||||||
|
'Automatycznie dodaj użytkowników';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersSubtitle =>
|
||||||
|
'Pozwól towarzyszowi automatycznie dodawać znalezione użytkowników.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersTitle =>
|
||||||
|
'Automatyczne dodawanie powtarzalników';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||||
|
'Zezwól na automatyczne dodawanie odkrytych repeaterów przez towarzysza.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersTitle =>
|
||||||
|
'Automatycznie dodaj serwery pokojowe';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||||
|
'Zezwól towarzyszowi na automatyczne dodawanie znalezionych serwerów pokojowych.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsTitle =>
|
||||||
|
'Automatycznie dodaj czujniki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||||
|
'Zezwól towarzyszowi na automatyczne dodawanie wykrytych czujników.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestTitle => 'Nadpisz najstarszy';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestSubtitle =>
|
||||||
|
'Gdy lista kontaktów jest pełna, najstarszy nieulubiony kontakt zostanie zastąpiony.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_Title => 'Odkryte Kontakty';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_noMatching => 'Brak pasujących kontaktów';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_searchHint => 'Wyszukaj odkryte kontakty';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_contactAdded => 'Kontakt dodany';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_addContact => 'Dodaj kontakt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_copyContact => 'Kopiuj kontakt do schowka';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContact => 'Usuń kontakt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAll =>
|
||||||
|
'Usuń wszystkie odkryte kontakty';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAllContent =>
|
||||||
|
'Czy na pewno chcesz usunąć wszystkie znalezione kontakty?';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get common_delete => 'Excluir';
|
String get common_delete => 'Excluir';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get common_deleteAll => 'Excluir Tudo';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get common_close => 'Fechar';
|
String get common_close => 'Fechar';
|
||||||
|
|
||||||
@@ -108,6 +111,91 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
String get scanner_title => 'MeshCore Open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceUsbLabel => 'USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Conecte via USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Selecione um dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Selecione um dispositivo USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'A comunicação serial via USB está ativa em dispositivos Android e plataformas de desktop compatíveis.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'Nenhum dispositivo USB encontrado. Conecte um e atualize.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied =>
|
||||||
|
'A permissão para acesso via USB foi negada.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing =>
|
||||||
|
'O dispositivo USB selecionado não está mais disponível.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Selecione um dispositivo USB válido.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy =>
|
||||||
|
'Já existe uma solicitação de conexão USB em andamento.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'Não há nenhum dispositivo USB conectado.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed =>
|
||||||
|
'Não foi possível abrir o dispositivo USB selecionado.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'Não foi possível conectar ao dispositivo USB selecionado.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'A comunicação serial via USB não é suportada nesta plataforma.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'A conexão USB já está ativa.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected =>
|
||||||
|
'Nenhum dispositivo USB foi selecionado.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'A conexão USB não está ativa.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName => 'Dispositivo de Serial para a Web';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Selecione um dispositivo USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Conectando ao dispositivo USB...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Procurando por dispositivos USB...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'Falha na conexão USB: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Procurando por dispositivos...';
|
String get scanner_scanning => 'Procurando por dispositivos...';
|
||||||
|
|
||||||
@@ -150,6 +238,13 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
String get scanner_bluetoothOffMessage =>
|
String get scanner_bluetoothOffMessage =>
|
||||||
'Por favor, ative o Bluetooth para escanear por dispositivos.';
|
'Por favor, ative o Bluetooth para escanear por dispositivos.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequired => 'Navegador Chrome necessário';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequiredMessage =>
|
||||||
|
'Esta aplicação web requer o Google Chrome ou um navegador baseado no Chromium para suporte de Bluetooth.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_enableBluetooth => 'Ative o Bluetooth';
|
String get scanner_enableBluetooth => 'Ative o Bluetooth';
|
||||||
|
|
||||||
@@ -234,6 +329,13 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_longitude => 'Longitude';
|
String get settings_longitude => 'Longitude';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettings => 'Configurações de Contato';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettingsSubtitle =>
|
||||||
|
'Configurações para como os contatos são adicionados';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_privacyMode => 'Modo de Privacidade';
|
String get settings_privacyMode => 'Modo de Privacidade';
|
||||||
|
|
||||||
@@ -324,6 +426,10 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
String get settings_aboutDescription =>
|
String get settings_aboutDescription =>
|
||||||
'Um cliente Flutter de código aberto para dispositivos de rede mesh LoRa Core da MeshCore.';
|
'Um cliente Flutter de código aberto para dispositivos de rede mesh LoRa Core da MeshCore.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_aboutOpenMeteoAttribution =>
|
||||||
|
'Dados de elevação LOS: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_infoName => 'Nome';
|
String get settings_infoName => 'Nome';
|
||||||
|
|
||||||
@@ -460,6 +566,14 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageUk => 'Ucraniano';
|
String get appSettings_languageUk => 'Ucraniano';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracing =>
|
||||||
|
'Ativar rastreamento de mensagens';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracingSubtitle =>
|
||||||
|
'Mostrar metadados detalhados de roteamento e tempo para as mensagens';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Notificações';
|
String get appSettings_notifications => 'Notificações';
|
||||||
|
|
||||||
@@ -620,6 +734,15 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_offlineMapCache => 'Cache de Mapa Offline';
|
String get appSettings_offlineMapCache => 'Cache de Mapa Offline';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsTitle => 'Unidades';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsMetric => 'Métrico (m/km)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsImperial => 'Imperial (ft/mi)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_noAreaSelected => 'Nenhuma área selecionada';
|
String get appSettings_noAreaSelected => 'Nenhuma área selecionada';
|
||||||
|
|
||||||
@@ -658,7 +781,35 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
'Os contatos serão exibidos quando os dispositivos anunciarem.';
|
'Os contatos serão exibidos quando os dispositivos anunciarem.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_searchContacts => 'Pesquisar contatos...';
|
String get contacts_unread => 'Não lido';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_searchContactsNoNumber => 'Pesquisar Contatos...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchContacts(int number, String str) {
|
||||||
|
return 'Pesquisar contatos...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchFavorites(int number, String str) {
|
||||||
|
return 'Pesquisar $number$str Favoritos...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchUsers(int number, String str) {
|
||||||
|
return 'Pesquisar $number$str Usuários...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRepeaters(int number, String str) {
|
||||||
|
return 'Pesquisar $number$str Repetidores...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRoomServers(int number, String str) {
|
||||||
|
return 'Pesquisar $number$str servidores de sala...';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_noUnreadContacts => 'Sem contatos não lidos.';
|
String get contacts_noUnreadContacts => 'Sem contatos não lidos.';
|
||||||
@@ -784,6 +935,12 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_editChannel => 'Editar canal';
|
String get channels_editChannel => 'Editar canal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_muteChannel => 'Silenciar canal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_unmuteChannel => 'Ativar canal';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_deleteChannel => 'Excluir canal';
|
String get channels_deleteChannel => 'Excluir canal';
|
||||||
|
|
||||||
@@ -792,6 +949,11 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
return 'Excluir \"$name\"? Não pode ser desfeito.';
|
return 'Excluir \"$name\"? Não pode ser desfeito.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channels_channelDeleteFailed(String name) {
|
||||||
|
return 'Falha ao excluir o canal \"$name\"';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String channels_channelDeleted(String name) {
|
String channels_channelDeleted(String name) {
|
||||||
return 'Canal \"$name\" excluído';
|
return 'Canal \"$name\" excluído';
|
||||||
@@ -1079,6 +1241,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_pathManagement => 'Gerenciamento de Caminhos';
|
String get chat_pathManagement => 'Gerenciamento de Caminhos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_ShowAllPaths => 'Mostrar todos os caminhos';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_routingMode => 'Modo de roteamento';
|
String get chat_routingMode => 'Modo de roteamento';
|
||||||
|
|
||||||
@@ -1237,6 +1402,12 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_title => 'Mapa de Nós';
|
String get map_title => 'Mapa de Nós';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_lineOfSight => 'Linha de visão';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_losScreenTitle => 'Linha de visão';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_noNodesWithLocation =>
|
String get map_noNodesWithLocation =>
|
||||||
'Não existem nós com dados de localização.';
|
'Não existem nós com dados de localização.';
|
||||||
@@ -1355,6 +1526,13 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_showSharedMarkers => 'Mostrar marcadores compartilhados';
|
String get map_showSharedMarkers => 'Mostrar marcadores compartilhados';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showGuessedLocations =>
|
||||||
|
'Mostrar as localizações dos nós estimados';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_guessedLocation => 'Localização estimada';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_lastSeenTime => 'Último Tempo de Visualização';
|
String get map_lastSeenTime => 'Último Tempo de Visualização';
|
||||||
|
|
||||||
@@ -1674,11 +1852,10 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Enviar comandos ao repetidor';
|
String get repeater_cliSubtitle => 'Enviar comandos ao repetidor';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Vizinhos';
|
String get repeater_neighbors => 'Vizinhos';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle =>
|
String get repeater_neighborsSubtitle => 'Visualizar vizinhos de salto zero.';
|
||||||
'Visualizar vizinhos de salto zero.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => 'Configurações';
|
String get repeater_settings => 'Configurações';
|
||||||
@@ -2377,7 +2554,7 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Repetidores Vizinhos';
|
String get neighbors_repeatersNeighbors => 'Repetidores Vizinhos';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Não estão disponíveis dados de vizinhos.';
|
String get neighbors_noData => 'Não estão disponíveis dados de vizinhos.';
|
||||||
@@ -2687,6 +2864,15 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get listFilter_all => 'Tudo';
|
String get listFilter_all => 'Tudo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_favorites => 'Favoritos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_addToFavorites => 'Adicionar aos favoritos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_removeFromFavorites => 'Remover da lista de favoritos';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_users => 'Usuários';
|
String get listFilter_users => 'Usuários';
|
||||||
|
|
||||||
@@ -2721,6 +2907,144 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get pathTrace_clearTooltip => 'Limpar caminho';
|
String get pathTrace_clearTooltip => 'Limpar caminho';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectStartEnd => 'Selecione nós iniciais e finais para LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losRunFailed(String error) {
|
||||||
|
return 'Falha na verificação da linha de visão: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losClearAllPoints => 'Limpe todos os pontos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRunToViewElevationProfile =>
|
||||||
|
'Execute o LOS para visualizar o perfil de elevação';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuTitle => 'Menu LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuSubtitle =>
|
||||||
|
'Toque nos nós ou mantenha pressionado o mapa para obter pontos personalizados';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowDisplayNodes => 'Mostrar nós de exibição';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losCustomPoints => 'Pontos personalizados';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losCustomPointLabel(int index) {
|
||||||
|
return '$index personalizado';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointA => 'Ponto A';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointB => 'Ponto B';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaA(String value, String unit) {
|
||||||
|
return 'Antena A: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaB(String value, String unit) {
|
||||||
|
return 'Antena B: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRun => 'Executar LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losNoElevationData => 'Sem dados de elevação';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileClear(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String clearance,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, limpar LOS, liberação mínima $clearance $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileBlocked(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, bloqueado por $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusChecking => 'LOS: verificando...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusNoData => 'LOS: sem dados';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||||
|
return 'LOS: $clear/$total limpo, $blocked bloqueado, $unknown desconhecido';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorElevationUnavailable =>
|
||||||
|
'Dados de elevação indisponíveis para uma ou mais amostras.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorInvalidInput =>
|
||||||
|
'Dados de pontos/elevação inválidos para cálculo de LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRenameCustomPoint => 'Renomear ponto personalizado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointName => 'Nome do ponto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowPanelTooltip => 'Mostrar painel LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losHidePanelTooltip => 'Ocultar painel LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losElevationAttribution =>
|
||||||
|
'Dados de elevação: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendRadioHorizon => 'Horizonte de rádio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendLosBeam => 'Linha de visada';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendTerrain => 'Terreno';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyLabel => 'Frequência';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyInfoTooltip => 'Ver detalhes do cálculo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyDialogTitle => 'Cálculo do horizonte de rádio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losFrequencyDialogDescription(
|
||||||
|
double baselineK,
|
||||||
|
double baselineFreq,
|
||||||
|
double frequencyMHz,
|
||||||
|
double kFactor,
|
||||||
|
) {
|
||||||
|
return 'Começando em k=$baselineK em $baselineFreq MHz, o cálculo ajusta o fator k para a banda atual de $frequencyMHz MHz, que define o limite do horizonte de rádio curvo.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_pathTrace => 'Traçado de Caminho';
|
String get contacts_pathTrace => 'Traçado de Caminho';
|
||||||
|
|
||||||
@@ -2890,4 +3214,90 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open exportação de dados de mapa GPX';
|
'meshcore-open exportação de dados de mapa GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Repetidores Próximos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Visto pela última vez';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_title => 'Configurações de contatos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddTitle => 'Descoberta Automática';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_otherTitle =>
|
||||||
|
'Outras configurações relacionadas a contatos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersTitle =>
|
||||||
|
'Adicionar usuários automaticamente';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersSubtitle =>
|
||||||
|
'Permitir que o companheiro adicione automaticamente os usuários descobertos.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersTitle =>
|
||||||
|
'Adicionar repetidores automaticamente';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||||
|
'Permitir que o companheiro adicione automaticamente os repetidores descobertos.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersTitle =>
|
||||||
|
'Adicionar automaticamente servidores de sala';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||||
|
'Permitir que o companheiro adicione automaticamente os servidores de salas descobertos.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsTitle =>
|
||||||
|
'Adicionar sensores automaticamente';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||||
|
'Permitir que o companheiro adicione automaticamente sensores descobertos.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestTitle =>
|
||||||
|
'Sobrescrever o Mais Antigo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestSubtitle =>
|
||||||
|
'Quando a lista de contatos estiver cheia, o contato mais antigo não favoritado será substituído.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_Title => 'Contatos Descobertos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_noMatching => 'Nenhum contato correspondente';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_searchHint => 'Pesquisar contatos descobertos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_contactAdded => 'Contato adicionado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_addContact => 'Adicionar Contato';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_copyContact =>
|
||||||
|
'Copiar Contato para a área de transferência';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContact => 'Excluir Contato';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAll =>
|
||||||
|
'Excluir Todos os Contatos Descobertos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAllContent =>
|
||||||
|
'Tem certeza de que deseja excluir todos os contatos descobertos?';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get common_delete => 'Удалить';
|
String get common_delete => 'Удалить';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get common_deleteAll => 'Удалить все';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get common_close => 'Закрыть';
|
String get common_close => 'Закрыть';
|
||||||
|
|
||||||
@@ -108,6 +111,92 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
String get scanner_title => 'MeshCore Open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceUsbLabel => 'USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Подключение через USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Выберите обнаруженное устройство с последовательным интерфейсом и подключите его напрямую к вашему узлу MeshCore.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Выберите USB-устройство';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'USB-серийный порт активен на поддерживаемых устройствах Android и на настольных платформах.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'Не обнаружено устройств USB. Подключите одно из них и обновите список.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied =>
|
||||||
|
'Запрос на доступ через USB был отклонен.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing =>
|
||||||
|
'Выбранное USB-устройство больше недоступно.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Выберите действительное USB-устройство.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy =>
|
||||||
|
'Еще одно запрошенное соединение через USB уже находится в процессе.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'Ни одно USB-устройство не подключено.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed =>
|
||||||
|
'Не удалось открыть выбранное USB-устройство.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'Не удалось установить соединение с выбранным USB-устройством.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'Поддержка последовательного USB отсутствует на данной платформе.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'USB-соединение уже установлено.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected =>
|
||||||
|
'Не было выбрано ни одно устройство USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'USB-соединение не установлено.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName =>
|
||||||
|
'Устройство для последовательного подключения к сети';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Выберите USB-устройство';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Подключение к USB-устройству...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Поиск USB-устройств...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'Не удалось установить соединение через USB: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Поиск устройств...';
|
String get scanner_scanning => 'Поиск устройств...';
|
||||||
|
|
||||||
@@ -149,6 +238,13 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
String get scanner_bluetoothOffMessage =>
|
String get scanner_bluetoothOffMessage =>
|
||||||
'Пожалуйста, включите Bluetooth, чтобы найти устройства.';
|
'Пожалуйста, включите Bluetooth, чтобы найти устройства.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequired => 'Требуется браузер Chrome';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequiredMessage =>
|
||||||
|
'Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_enableBluetooth => 'Включите Bluetooth';
|
String get scanner_enableBluetooth => 'Включите Bluetooth';
|
||||||
|
|
||||||
@@ -232,6 +328,13 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_longitude => 'Долгота';
|
String get settings_longitude => 'Долгота';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettings => 'Настройки контактов';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettingsSubtitle =>
|
||||||
|
'Настройки добавления контактов';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_privacyMode => 'Режим конфиденциальности';
|
String get settings_privacyMode => 'Режим конфиденциальности';
|
||||||
|
|
||||||
@@ -321,6 +424,10 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
String get settings_aboutDescription =>
|
String get settings_aboutDescription =>
|
||||||
'Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.';
|
'Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_aboutOpenMeteoAttribution =>
|
||||||
|
'Данные о высоте LOS: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_infoName => 'Имя';
|
String get settings_infoName => 'Имя';
|
||||||
|
|
||||||
@@ -458,6 +565,14 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageUk => 'Українська';
|
String get appSettings_languageUk => 'Українська';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracing =>
|
||||||
|
'Включить трассировку сообщений';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracingSubtitle =>
|
||||||
|
'Показывать подробные метаданные о маршрутизации и времени для сообщений';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Уведомления';
|
String get appSettings_notifications => 'Уведомления';
|
||||||
|
|
||||||
@@ -620,6 +735,15 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_offlineMapCache => 'Кэш офлайн-карты';
|
String get appSettings_offlineMapCache => 'Кэш офлайн-карты';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsTitle => 'Единицы';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsMetric => 'Метрическая (м/км)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsImperial => 'Имперская (ft / mi)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_noAreaSelected => 'Область не выбрана';
|
String get appSettings_noAreaSelected => 'Область не выбрана';
|
||||||
|
|
||||||
@@ -657,7 +781,35 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
'Контакты появятся, когда устройства начнут рассылать оповещения';
|
'Контакты появятся, когда устройства начнут рассылать оповещения';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_searchContacts => 'Поиск контактов...';
|
String get contacts_unread => 'Непрочитанное';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_searchContactsNoNumber => 'Поиск контактов...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchContacts(int number, String str) {
|
||||||
|
return 'Поиск контактов...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchFavorites(int number, String str) {
|
||||||
|
return 'Поиск $number$str избранного...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchUsers(int number, String str) {
|
||||||
|
return 'Поиск $number$str пользователей...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRepeaters(int number, String str) {
|
||||||
|
return 'Поиск $number$str ретрансляторов...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRoomServers(int number, String str) {
|
||||||
|
return 'Поиск $number$str серверов комнат...';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_noUnreadContacts => 'Нет непрочитанных контактов';
|
String get contacts_noUnreadContacts => 'Нет непрочитанных контактов';
|
||||||
@@ -782,6 +934,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_editChannel => 'Изменить канал';
|
String get channels_editChannel => 'Изменить канал';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_muteChannel => 'Отключить уведомления канала';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_unmuteChannel => 'Включить уведомления канала';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_deleteChannel => 'Удалить канал';
|
String get channels_deleteChannel => 'Удалить канал';
|
||||||
|
|
||||||
@@ -790,6 +948,11 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
return 'Удалить \"$name\"? Это действие нельзя отменить.';
|
return 'Удалить \"$name\"? Это действие нельзя отменить.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channels_channelDeleteFailed(String name) {
|
||||||
|
return 'Не удалось удалить канал $name.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String channels_channelDeleted(String name) {
|
String channels_channelDeleted(String name) {
|
||||||
return 'Канал \"$name\" удалён';
|
return 'Канал \"$name\" удалён';
|
||||||
@@ -1077,6 +1240,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_pathManagement => 'Управление маршрутами';
|
String get chat_pathManagement => 'Управление маршрутами';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_ShowAllPaths => 'Показать все пути';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_routingMode => 'Режим маршрутизации';
|
String get chat_routingMode => 'Режим маршрутизации';
|
||||||
|
|
||||||
@@ -1239,6 +1405,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_title => 'Карта нод';
|
String get map_title => 'Карта нод';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_lineOfSight => 'Линия видимости';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_losScreenTitle => 'Линия видимости';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_noNodesWithLocation => 'Нет нод с данными о местоположении';
|
String get map_noNodesWithLocation => 'Нет нод с данными о местоположении';
|
||||||
|
|
||||||
@@ -1356,6 +1528,13 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_showSharedMarkers => 'Показывать общие метки';
|
String get map_showSharedMarkers => 'Показывать общие метки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showGuessedLocations =>
|
||||||
|
'Отобразить предполагаемые места расположения узлов';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_guessedLocation => 'Угаданное место';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_lastSeenTime => 'Время последнего появления';
|
String get map_lastSeenTime => 'Время последнего появления';
|
||||||
|
|
||||||
@@ -1676,10 +1855,10 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Отправка команд репитеру';
|
String get repeater_cliSubtitle => 'Отправка команд репитеру';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Соседи';
|
String get repeater_neighbors => 'Соседи';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle => 'Просмотр соседей на нулевом хопе.';
|
String get repeater_neighborsSubtitle => 'Просмотр соседей на нулевом хопе.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => 'Настройки';
|
String get repeater_settings => 'Настройки';
|
||||||
@@ -2379,7 +2558,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Соседи репитеров';
|
String get neighbors_repeatersNeighbors => 'Соседи репитеров';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Данные о соседях недоступны.';
|
String get neighbors_noData => 'Данные о соседях недоступны.';
|
||||||
@@ -2689,6 +2868,15 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get listFilter_all => 'Все';
|
String get listFilter_all => 'Все';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_favorites => 'Избранное';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_addToFavorites => 'Добавить в избранное';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_removeFromFavorites => 'Удалить из избранного';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_users => 'Пользователи';
|
String get listFilter_users => 'Пользователи';
|
||||||
|
|
||||||
@@ -2723,6 +2911,144 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get pathTrace_clearTooltip => 'Очистить путь';
|
String get pathTrace_clearTooltip => 'Очистить путь';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectStartEnd => 'Выберите начальный и конечный узлы для LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losRunFailed(String error) {
|
||||||
|
return 'Проверка прямой видимости не удалась: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losClearAllPoints => 'Очистить все точки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRunToViewElevationProfile =>
|
||||||
|
'Запустите LOS, чтобы просмотреть профиль высот.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuTitle => 'ЛОС Меню';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuSubtitle =>
|
||||||
|
'Коснитесь узлов или нажмите и удерживайте карту для выбора пользовательских точек.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowDisplayNodes => 'Показать узлы отображения';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losCustomPoints => 'Пользовательские точки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losCustomPointLabel(int index) {
|
||||||
|
return 'Пользовательский $index';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointA => 'Точка А';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointB => 'Точка Б';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaA(String value, String unit) {
|
||||||
|
return 'Антенна А: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaB(String value, String unit) {
|
||||||
|
return 'Антенна Б: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRun => 'Запустить ЛОС';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losNoElevationData => 'Нет данных о высоте';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileClear(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String clearance,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, свободная зона видимости, минимальный зазор $clearance $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileBlocked(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, заблокирован $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusChecking => 'ЛОС: проверяю...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusNoData => 'ЛОС: нет данных';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||||
|
return 'LOS: $clear/$total очищено, $blocked заблокировано, $unknown неизвестно.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorElevationUnavailable =>
|
||||||
|
'Данные о высоте недоступны для одного или нескольких образцов.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorInvalidInput =>
|
||||||
|
'Неверные данные о точках/высоте для расчета LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRenameCustomPoint => 'Переименовать пользовательскую точку';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointName => 'Имя точки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowPanelTooltip => 'Показать панель LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losHidePanelTooltip => 'Скрыть панель LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losElevationAttribution =>
|
||||||
|
'Данные о высоте: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendRadioHorizon => 'Радиогоризонт';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendLosBeam => 'Линия прямой видимости';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendTerrain => 'Рельеф';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyLabel => 'Частота';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyInfoTooltip => 'Просмотреть детали расчёта';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyDialogTitle => 'Расчёт радиогоризонта';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losFrequencyDialogDescription(
|
||||||
|
double baselineK,
|
||||||
|
double baselineFreq,
|
||||||
|
double frequencyMHz,
|
||||||
|
double kFactor,
|
||||||
|
) {
|
||||||
|
return 'Начиная с k=$baselineK на частоте $baselineFreq МГц, расчет корректирует коэффициент k для текущего диапазона $frequencyMHz МГц, который определяет изогнутую границу радиогоризонта.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_pathTrace => 'Трассировка пути';
|
String get contacts_pathTrace => 'Трассировка пути';
|
||||||
|
|
||||||
@@ -2901,4 +3227,90 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open экспорт данных карты GPX';
|
'meshcore-open экспорт данных карты GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Ближайшие ретрансляторы';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Последний раз видели';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_title => 'Настройки контактов';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddTitle => 'Автоматическое обнаружение';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_otherTitle =>
|
||||||
|
'Другие настройки, связанные с контактами';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersTitle =>
|
||||||
|
'Автоматически добавлять пользователей';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersSubtitle =>
|
||||||
|
'Разрешить компаньону автоматически добавлять обнаруженных пользователей';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersTitle =>
|
||||||
|
'Автоматически добавлять ретрансляторы';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||||
|
'Разрешить спутнику автоматически добавлять обнаруженные ретрансляторы';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersTitle =>
|
||||||
|
'Автоматически добавлять серверы комнат';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||||
|
'Разрешить компаньону автоматически добавлять обнаруженные сервера комнат.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsTitle =>
|
||||||
|
'Автоматически добавлять датчики';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||||
|
'Разрешить компаньону автоматически добавлять обнаруженные датчики';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestTitle =>
|
||||||
|
'Перезаписать самое старое';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestSubtitle =>
|
||||||
|
'Когда список контактов заполнен, будет заменен самый старый контакт, который не находится в избранном.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_Title => 'Обнаруженные контакты';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_noMatching => 'Нет совпадающих контактов';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_searchHint => 'Найденные контакты поиска';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_contactAdded => 'Контакт добавлен';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_addContact => 'Добавить контакт';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_copyContact =>
|
||||||
|
'Копировать контакт в буфер обмена';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContact => 'Удалить контакт';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAll =>
|
||||||
|
'Удалить Все Обнаруженные Контакты';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAllContent =>
|
||||||
|
'Вы уверены, что хотите удалить все обнаруженные контакты?';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get common_delete => 'Odstrániť';
|
String get common_delete => 'Odstrániť';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get common_deleteAll => 'Zmazať všetko';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get common_close => 'Zavrieť';
|
String get common_close => 'Zavrieť';
|
||||||
|
|
||||||
@@ -108,6 +111,91 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
String get scanner_title => 'MeshCore Open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceUsbLabel => 'USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Pripojte cez USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vašej MeshCore uzlu.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Vyberte USB zariadenie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied =>
|
||||||
|
'Žiadosť o prístup cez USB bola zamietnutá.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing =>
|
||||||
|
'Vybrané USB zariadenie už nie je dostupné.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Vyberte platné USB zariadenie.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy =>
|
||||||
|
'Ďalšia požiadavka na pripojenie cez USB je aktuálne v procese.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'Nie je pripojené žiadne USB zariadenie.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed =>
|
||||||
|
'Nepodarilo sa otvoriť vybrané USB zariadenie.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'Nepodarilo sa sa sa pripojiť k vybranému USB zariadeniu.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'Podpora USB sériového rozhrania nie je na tejto platforme dostupná.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'Pripojenie cez USB je už aktivované.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected =>
|
||||||
|
'Nebolo vybrané žiadne USB zariadenie.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'Pripojenie cez USB nie je aktivované.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName => 'Webový sériový zariadenie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Vyberte USB zariadenie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Pripojenie k USB zariadeniu...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Hľadanie USB zariadení...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'Neúspešné pripojenie cez USB: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Skrívania zariadení...';
|
String get scanner_scanning => 'Skrívania zariadení...';
|
||||||
|
|
||||||
@@ -150,6 +238,13 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
String get scanner_bluetoothOffMessage =>
|
String get scanner_bluetoothOffMessage =>
|
||||||
'Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.';
|
'Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequired => 'Vyžaduje sa prehliadač Chrome';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequiredMessage =>
|
||||||
|
'Táto webová aplikácia vyžaduje Google Chrome alebo prehliadač založený na Chromium pre podporu Bluetooth.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_enableBluetooth => 'Povolte Bluetooth';
|
String get scanner_enableBluetooth => 'Povolte Bluetooth';
|
||||||
|
|
||||||
@@ -233,6 +328,13 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_longitude => 'Dĺžka';
|
String get settings_longitude => 'Dĺžka';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettings => 'Nastavenia kontaktov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettingsSubtitle =>
|
||||||
|
'Nastavenia pre pridávanie kontaktov.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_privacyMode => 'Režim ochrany súkromia';
|
String get settings_privacyMode => 'Režim ochrany súkromia';
|
||||||
|
|
||||||
@@ -320,6 +422,10 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
String get settings_aboutDescription =>
|
String get settings_aboutDescription =>
|
||||||
'Otvorený zdrojový Flutter klient pre MeshCore LoRa sieťové zariadenia.';
|
'Otvorený zdrojový Flutter klient pre MeshCore LoRa sieťové zariadenia.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_aboutOpenMeteoAttribution =>
|
||||||
|
'Údaje o nadmorskej výške LOS: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_infoName => 'Meno';
|
String get settings_infoName => 'Meno';
|
||||||
|
|
||||||
@@ -456,6 +562,13 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageUk => 'Ukrajinská';
|
String get appSettings_languageUk => 'Ukrajinská';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracing => 'Povoliť sledovanie správ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracingSubtitle =>
|
||||||
|
'Zobraziť podrobné metadáta o smerovaní a časovaní správ';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Upozornenia';
|
String get appSettings_notifications => 'Upozornenia';
|
||||||
|
|
||||||
@@ -614,6 +727,15 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_offlineMapCache => 'Offline Mapa Pamäť';
|
String get appSettings_offlineMapCache => 'Offline Mapa Pamäť';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsTitle => 'Jednotky';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsMetric => 'Metrické (m / km)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsImperial => 'Imperiálne (ft / mi)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_noAreaSelected => 'Neoznačila sa žiadna oblasť';
|
String get appSettings_noAreaSelected => 'Neoznačila sa žiadna oblasť';
|
||||||
|
|
||||||
@@ -651,7 +773,35 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
'Kontakty sa zobrazia, keď zariadenia spúšťajú reklamu.';
|
'Kontakty sa zobrazia, keď zariadenia spúšťajú reklamu.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_searchContacts => 'Vyhľadávajte kontakty...';
|
String get contacts_unread => 'Neprečítané';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_searchContactsNoNumber => 'Hľadať kontakty...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchContacts(int number, String str) {
|
||||||
|
return 'Vyhľadávajte kontakty...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchFavorites(int number, String str) {
|
||||||
|
return 'Hľadať $number$str obľúbené...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchUsers(int number, String str) {
|
||||||
|
return 'Hľadať $number$str používateľov...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRepeaters(int number, String str) {
|
||||||
|
return 'Hľadať $number$str opakovače...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRoomServers(int number, String str) {
|
||||||
|
return 'Hľadaj $number$str serverov miestností...';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_noUnreadContacts => 'Žiadne neprečítané kontakty';
|
String get contacts_noUnreadContacts => 'Žiadne neprečítané kontakty';
|
||||||
@@ -779,6 +929,12 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_editChannel => 'Upraviť kanál';
|
String get channels_editChannel => 'Upraviť kanál';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_muteChannel => 'Stlmiť kanál';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_unmuteChannel => 'Zrušiť stlmenie kanála';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_deleteChannel => 'Odstrániť kanál';
|
String get channels_deleteChannel => 'Odstrániť kanál';
|
||||||
|
|
||||||
@@ -787,6 +943,11 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
return 'Odstrániť \"$name\"? To sa nedá zrušiť.';
|
return 'Odstrániť \"$name\"? To sa nedá zrušiť.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channels_channelDeleteFailed(String name) {
|
||||||
|
return 'Kanál \"$name\" sa nepodarilo odstrániť';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String channels_channelDeleted(String name) {
|
String channels_channelDeleted(String name) {
|
||||||
return 'Kanál \"$name\" bol odstránený';
|
return 'Kanál \"$name\" bol odstránený';
|
||||||
@@ -1074,6 +1235,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_pathManagement => 'Správa ciest';
|
String get chat_pathManagement => 'Správa ciest';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_ShowAllPaths => 'Zobraziť všetky cesty';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_routingMode => 'Režim trasy';
|
String get chat_routingMode => 'Režim trasy';
|
||||||
|
|
||||||
@@ -1233,6 +1397,12 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_title => 'Mapa uzlov';
|
String get map_title => 'Mapa uzlov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_lineOfSight => 'Line of Sight';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_losScreenTitle => 'Line of Sight';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_noNodesWithLocation => 'Žiadne uzly s údajmi o polohe';
|
String get map_noNodesWithLocation => 'Žiadne uzly s údajmi o polohe';
|
||||||
|
|
||||||
@@ -1350,6 +1520,13 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_showSharedMarkers => 'Zobraziť zdieľané značky';
|
String get map_showSharedMarkers => 'Zobraziť zdieľané značky';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showGuessedLocations =>
|
||||||
|
'Zobraziť umiestnenia odhadnutých uzlov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_guessedLocation => 'Odhadnutá lokalita';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_lastSeenTime => 'Posledný čas sledovania';
|
String get map_lastSeenTime => 'Posledný čas sledovania';
|
||||||
|
|
||||||
@@ -1669,10 +1846,10 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Pošlite príkazy opakovaču';
|
String get repeater_cliSubtitle => 'Pošlite príkazy opakovaču';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Súsezný';
|
String get repeater_neighbors => 'Súsezný';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle => 'Zobraziť susedné body bez skokov.';
|
String get repeater_neighborsSubtitle => 'Zobraziť susedné body bez skokov.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => 'Nastavenia';
|
String get repeater_settings => 'Nastavenia';
|
||||||
@@ -2363,7 +2540,7 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Opakovadlá Súsezná';
|
String get neighbors_repeatersNeighbors => 'Opakovadlá Súsezná';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData =>
|
String get neighbors_noData =>
|
||||||
@@ -2672,6 +2849,15 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get listFilter_all => 'Všetko';
|
String get listFilter_all => 'Všetko';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_favorites => 'Obľúbené';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_addToFavorites => 'Pridaj do obľúbených';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_removeFromFavorites => 'Odstrániť z označení';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_users => 'Používatelia';
|
String get listFilter_users => 'Používatelia';
|
||||||
|
|
||||||
@@ -2706,6 +2892,144 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get pathTrace_clearTooltip => 'Zmazať cestu';
|
String get pathTrace_clearTooltip => 'Zmazať cestu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectStartEnd => 'Vyberte počiatočný a koncový uzol pre LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losRunFailed(String error) {
|
||||||
|
return 'Kontrola priamej viditeľnosti zlyhala: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losClearAllPoints => 'Vymazať všetky body';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRunToViewElevationProfile =>
|
||||||
|
'Ak chcete zobraziť výškový profil, spustite LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuTitle => 'Menu LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuSubtitle =>
|
||||||
|
'Klepnutím na uzly alebo dlhým stlačením mapy získate vlastné body';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowDisplayNodes => 'Zobraziť uzly zobrazenia';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losCustomPoints => 'Vlastné body';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losCustomPointLabel(int index) {
|
||||||
|
return 'Vlastné $index';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointA => 'Bod A';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointB => 'Bod B';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaA(String value, String unit) {
|
||||||
|
return 'Anténa A: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaB(String value, String unit) {
|
||||||
|
return 'Anténa B: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRun => 'Spustite LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losNoElevationData => 'Žiadne údaje o nadmorskej výške';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileClear(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String clearance,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, vymazať LOS, min. vôľa $clearance $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileBlocked(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, blokovaný $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusChecking => 'LOS: kontrolujem...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusNoData => 'LOS: žiadne údaje';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||||
|
return 'LOS: $clear/$total vymazané, $blocked blokované, $unknown neznáme';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorElevationUnavailable =>
|
||||||
|
'Údaje o nadmorskej výške nie sú k dispozícii pre jednu alebo viacero vzoriek.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorInvalidInput =>
|
||||||
|
'Neplatné body/údaje o nadmorskej výške pre výpočet LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRenameCustomPoint => 'Premenovať vlastný bod';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointName => 'Názov bodu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowPanelTooltip => 'Zobraziť panel LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losHidePanelTooltip => 'Skryť panel LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losElevationAttribution =>
|
||||||
|
'Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendRadioHorizon => 'Rádiový horizont';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendLosBeam => 'Priama viditeľnosť';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendTerrain => 'Terén';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyLabel => 'Frekvencia';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyInfoTooltip => 'Zobraziť podrobnosti výpočtu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyDialogTitle => 'Výpočet rádiového horizontu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losFrequencyDialogDescription(
|
||||||
|
double baselineK,
|
||||||
|
double baselineFreq,
|
||||||
|
double frequencyMHz,
|
||||||
|
double kFactor,
|
||||||
|
) {
|
||||||
|
return 'Počnúc od k=$baselineK pri $baselineFreq MHz výpočet upraví k-faktor pre aktuálne pásmo $frequencyMHz MHz, ktorý definuje zakrivený strop rádiového horizontu.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_pathTrace => 'Sledovanie lúčov';
|
String get contacts_pathTrace => 'Sledovanie lúčov';
|
||||||
|
|
||||||
@@ -2877,4 +3201,88 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open export dát GPX mapových údajov';
|
'meshcore-open export dát GPX mapových údajov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Miestne opakovače';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Naposledy videný';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_title => 'Nastavenia kontaktov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddTitle => 'Automatické zisťovanie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_otherTitle =>
|
||||||
|
'Ďalšie nastavenia súvisiace s kontaktami';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersTitle =>
|
||||||
|
'Automaticky pridávať užívateľov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersSubtitle =>
|
||||||
|
'Povoliť spoločníkovi automaticky pridávať objavených užívateľov.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersTitle =>
|
||||||
|
'Automaticky pridávať opakovače';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||||
|
'Povoliť spoločníkovi automaticky pridávať objavené repeater.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersTitle =>
|
||||||
|
'Automaticky pridávať server miestnosti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||||
|
'Povoliť spoločníkovi automaticky pridať objavené serverové miestnosti.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsTitle =>
|
||||||
|
'Automaticky pridávať senzory';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||||
|
'Povoliť spoločníkovi automaticky pridávať objavené senzory.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestTitle => 'Prepísať najstaršie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestSubtitle =>
|
||||||
|
'Keď je zoznam kontaktov plný, bude nahradený najstarší neoznačený kontakt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_Title => 'Objavené kontakty';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_noMatching => 'Žiadne zhodné kontakty';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_searchHint => 'Vyhľadať objavené kontakty';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_contactAdded => 'Kontakt bol pridaný';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_addContact => 'Pridať kontakt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_copyContact => 'Kopírovať kontakt do schránky';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContact => 'Zmazať kontakt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAll =>
|
||||||
|
'Zmazať všetky objavené kontakty';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAllContent =>
|
||||||
|
'Ste si istí, že chcete zmazať všetky objavené kontakty?';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get common_delete => 'Izbrisati';
|
String get common_delete => 'Izbrisati';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get common_deleteAll => 'Izbriši vse';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get common_close => 'Zapri';
|
String get common_close => 'Zapri';
|
||||||
|
|
||||||
@@ -108,6 +111,89 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
String get scanner_title => 'MeshCore Open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceUsbLabel => 'USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Povežite preko USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Izberite zaznano serijsko napravo in se neposredno povežite z vašo MeshCore napravo.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Izberite USB naprave';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'USB serijska povezava je aktivna na podprtih napravah Android in na desktop platformah.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'Niti en USB naprave niso najdeni. Povežite eno in posodobite.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied =>
|
||||||
|
'Dovoljenje za dostop preko USB-ja je bilo zavrnjeno.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing => 'Izbrani USB napravej je več ne.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Izberite veljavno USB naprave.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy => 'Že je v teku zahteva za povezavo preko USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'Ni priklopljenih USB naprave.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed =>
|
||||||
|
'Uspešno ni bilo mogo, da se odpre izbran naprave USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'Niso bilo mogoče uskladiti povezave z izbranim USB napom.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'USB serijska komunikacija ni podprta na tej platformi.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'USB povezava je že aktivirana.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected => 'Ni bilo izbranega USB naprave.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'USB povezava ni aktivirana.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName =>
|
||||||
|
'Naprave za serijsko komunikacijo preko spleta';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Izberite USB naprave.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Povezava z USB napravo...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Iskanje USB naprav...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'Napaka pri povezavi preko USB: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Skeniram za naprave...';
|
String get scanner_scanning => 'Skeniram za naprave...';
|
||||||
|
|
||||||
@@ -150,6 +236,13 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
String get scanner_bluetoothOffMessage =>
|
String get scanner_bluetoothOffMessage =>
|
||||||
'Prosimo, vklopite Bluetooth, da lahko poiščete naprave.';
|
'Prosimo, vklopite Bluetooth, da lahko poiščete naprave.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequired => 'Zahtevan brskalnik Chrome';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequiredMessage =>
|
||||||
|
'Ta spletna aplikacija za podporo Bluetooth zahteva Google Chrome ali brskalnik na osnovi Chromiuma.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_enableBluetooth => 'Omogočite Bluetooth';
|
String get scanner_enableBluetooth => 'Omogočite Bluetooth';
|
||||||
|
|
||||||
@@ -233,6 +326,13 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_longitude => 'Dolžina';
|
String get settings_longitude => 'Dolžina';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettings => 'Nastavitve stika';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettingsSubtitle =>
|
||||||
|
'Nastavitve za dodajanje stikov.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_privacyMode => 'Zasebnost';
|
String get settings_privacyMode => 'Zasebnost';
|
||||||
|
|
||||||
@@ -319,6 +419,10 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
String get settings_aboutDescription =>
|
String get settings_aboutDescription =>
|
||||||
'Odprtokodni Flutter klient za naprave za LoRa omrežje MeshCore.';
|
'Odprtokodni Flutter klient za naprave za LoRa omrežje MeshCore.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_aboutOpenMeteoAttribution =>
|
||||||
|
'Podatki o višini LOS: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_infoName => 'Ime';
|
String get settings_infoName => 'Ime';
|
||||||
|
|
||||||
@@ -455,6 +559,13 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageUk => 'Ukrajinsko';
|
String get appSettings_languageUk => 'Ukrajinsko';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracing => 'Omogoči sledenje sporočilom';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracingSubtitle =>
|
||||||
|
'Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Obvestila';
|
String get appSettings_notifications => 'Obvestila';
|
||||||
|
|
||||||
@@ -615,6 +726,15 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_offlineMapCache => 'Shramba zemljevidov brez povezave';
|
String get appSettings_offlineMapCache => 'Shramba zemljevidov brez povezave';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsTitle => 'Enote';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsMetric => 'Metrična (m/km)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsImperial => 'Imperialno (ft / mi)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_noAreaSelected => 'Območje ni izbrano';
|
String get appSettings_noAreaSelected => 'Območje ni izbrano';
|
||||||
|
|
||||||
@@ -652,7 +772,35 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
'Stiki se bodo prikazali, ko se naprave oglasijo.';
|
'Stiki se bodo prikazali, ko se naprave oglasijo.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_searchContacts => 'Iskanje stikov...';
|
String get contacts_unread => 'Neprebrano';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_searchContactsNoNumber => 'Iskanje stikov...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchContacts(int number, String str) {
|
||||||
|
return 'Iskanje stikov...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchFavorites(int number, String str) {
|
||||||
|
return 'Iskanje $number$str priljubljenih...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchUsers(int number, String str) {
|
||||||
|
return 'Išči $number$str uporabnikov...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRepeaters(int number, String str) {
|
||||||
|
return 'Išči $number$str ponavljalnike...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRoomServers(int number, String str) {
|
||||||
|
return 'Išči $number$str strežnikov sob...';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_noUnreadContacts => 'Ne prebrani stiki.';
|
String get contacts_noUnreadContacts => 'Ne prebrani stiki.';
|
||||||
@@ -777,6 +925,12 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_editChannel => 'Uredi kanal';
|
String get channels_editChannel => 'Uredi kanal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_muteChannel => 'Utišaj kanal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_unmuteChannel => 'Vklopi obvestila kanala';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_deleteChannel => 'Pošlji kanal';
|
String get channels_deleteChannel => 'Pošlji kanal';
|
||||||
|
|
||||||
@@ -785,6 +939,11 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
return 'Izbrišem \"$name\"? To se ne da povrniti.';
|
return 'Izbrišem \"$name\"? To se ne da povrniti.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channels_channelDeleteFailed(String name) {
|
||||||
|
return 'Kanala $name ni bilo mogoče izbrisati';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String channels_channelDeleted(String name) {
|
String channels_channelDeleted(String name) {
|
||||||
return 'Kanal \"$name\" izbrisan.';
|
return 'Kanal \"$name\" izbrisan.';
|
||||||
@@ -1072,6 +1231,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_pathManagement => 'Upravljanje poti';
|
String get chat_pathManagement => 'Upravljanje poti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_ShowAllPaths => 'Prikaži vse poti';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_routingMode => 'Navodilo za usmerjevalni način';
|
String get chat_routingMode => 'Navodilo za usmerjevalni način';
|
||||||
|
|
||||||
@@ -1228,6 +1390,12 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_title => 'Mapa omrežja';
|
String get map_title => 'Mapa omrežja';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_lineOfSight => 'Linija vida';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_losScreenTitle => 'Linija vida';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_noNodesWithLocation =>
|
String get map_noNodesWithLocation =>
|
||||||
'Nihče od notranjih elementov nima podatkov o lokaciji.';
|
'Nihče od notranjih elementov nima podatkov o lokaciji.';
|
||||||
@@ -1346,6 +1514,12 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_showSharedMarkers => 'Pokaži skupno označenja';
|
String get map_showSharedMarkers => 'Pokaži skupno označenja';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showGuessedLocations => 'Pokaži lokacije domnevnih not.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_guessedLocation => 'Predpostavljena lokacija';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_lastSeenTime => 'Datum zadnjega vpogleda';
|
String get map_lastSeenTime => 'Datum zadnjega vpogleda';
|
||||||
|
|
||||||
@@ -1668,10 +1842,10 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
'Pošlji ukazne povelje na ponovitveno enoto.';
|
'Pošlji ukazne povelje na ponovitveno enoto.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Sosedi';
|
String get repeater_neighbors => 'Sosedi';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle => 'Pogledati nič sosednjih hopjev.';
|
String get repeater_neighborsSubtitle => 'Pogledati nič sosednjih hopjev.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => 'Nastavitve';
|
String get repeater_settings => 'Nastavitve';
|
||||||
@@ -2367,7 +2541,7 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Ponovitve Sosedi';
|
String get neighbors_repeatersNeighbors => 'Ponovitve Sosedi';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Niso na voljo podatki o sosedih.';
|
String get neighbors_noData => 'Niso na voljo podatki o sosedih.';
|
||||||
@@ -2675,6 +2849,15 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get listFilter_all => 'Vse';
|
String get listFilter_all => 'Vse';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_favorites => 'Priljubljene';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_addToFavorites => 'Dodaj v priljubljene';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_removeFromFavorites => 'Odstrani iz priljubljenih';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_users => 'Uporabniki';
|
String get listFilter_users => 'Uporabniki';
|
||||||
|
|
||||||
@@ -2709,6 +2892,144 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get pathTrace_clearTooltip => 'Počisti pot';
|
String get pathTrace_clearTooltip => 'Počisti pot';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectStartEnd => 'Izberite začetno in končno vozlišče za LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losRunFailed(String error) {
|
||||||
|
return 'Preverjanje vidnega polja ni uspelo: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losClearAllPoints => 'Počisti vse točke';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRunToViewElevationProfile =>
|
||||||
|
'Zaženite LOS za ogled višinskega profila';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuTitle => 'LOS meni';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuSubtitle =>
|
||||||
|
'Tapnite vozlišča ali dolgo pritisnite na zemljevid za točke po meri';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowDisplayNodes => 'Pokaži prikazna vozlišča';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losCustomPoints => 'Točke po meri';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losCustomPointLabel(int index) {
|
||||||
|
return 'Po meri $index';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointA => 'Točka A';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointB => 'Točka B';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaA(String value, String unit) {
|
||||||
|
return 'Antena A: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaB(String value, String unit) {
|
||||||
|
return 'Antena B: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRun => 'Zaženi LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losNoElevationData => 'Ni podatkov o višini';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileClear(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String clearance,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, čisti LOS, najmanjša razdalja $clearance $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileBlocked(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, blokiral $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusChecking => 'LOS: preverjam ...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusNoData => 'LOS: ni podatkov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||||
|
return 'LOS: $clear/$total jasno, $blocked blokirano, $unknown neznano';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorElevationUnavailable =>
|
||||||
|
'Podatki o nadmorski višini niso na voljo za enega ali več vzorcev.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorInvalidInput =>
|
||||||
|
'Neveljavni podatki o točkah/višini za izračun LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRenameCustomPoint => 'Preimenujte točko po meri';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointName => 'Ime točke';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowPanelTooltip => 'Pokaži ploščo LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losHidePanelTooltip => 'Skrij ploščo LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losElevationAttribution =>
|
||||||
|
'Podatki o višini: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendRadioHorizon => 'Radijski horizont';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendLosBeam => 'Linija vidnosti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendTerrain => 'Teren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyLabel => 'Frekvenca';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyInfoTooltip => 'Prikaži podrobnosti izračuna';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyDialogTitle => 'Izračun radijskega horizonta';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losFrequencyDialogDescription(
|
||||||
|
double baselineK,
|
||||||
|
double baselineFreq,
|
||||||
|
double frequencyMHz,
|
||||||
|
double kFactor,
|
||||||
|
) {
|
||||||
|
return 'Začenši od k=$baselineK pri $baselineFreq MHz, izračun prilagodi k-faktor za trenutni pas $frequencyMHz MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_pathTrace => 'Sledenje poti';
|
String get contacts_pathTrace => 'Sledenje poti';
|
||||||
|
|
||||||
@@ -2882,4 +3203,87 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open izvoz podatkov GPX karte';
|
'meshcore-open izvoz podatkov GPX karte';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Bližnji ponovitelji';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Zadnjič videno';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_title => 'Nastavitve stikov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddTitle => 'Avtomatsko odkrivanje';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_otherTitle => 'Druge nastavitve v zvezi s stiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersTitle =>
|
||||||
|
'Avtomatsko dodaj uporabnike';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersSubtitle =>
|
||||||
|
'Dovoli spremljevalcu, da samodejno doda odkrite uporabnike.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersTitle =>
|
||||||
|
'Avtomatsko dodaj ponovitelje';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||||
|
'Dovoli spremljevalcu, da samodejno doda odkrite ponovitelje.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersTitle =>
|
||||||
|
'Avtomatsko dodaj strežnike sob';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||||
|
'Dovoli spremljevalcu, da samodejno doda odkrite strežnike sob.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsTitle =>
|
||||||
|
'Avtomatsko dodaj senzorje';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||||
|
'Dovoli spremljevalcu, da samodejno doda odkrite senzorje.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestTitle => 'Prepiši najstarejše';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestSubtitle =>
|
||||||
|
'Ko je seznam stikov poln, bo najstarejši nestarševski stik zamenjan.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_Title => 'Odkriti stiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_noMatching => 'Ni ujemajočih stikov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_searchHint => 'Najdeni stiki po iskanju';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_contactAdded => 'Kontakt dodan';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_addContact => 'Dodaj stik';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_copyContact => 'Kopiraj stik v odložišče';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContact => 'Izbriši stik';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAll =>
|
||||||
|
'Izbriši vse odkrite kontakte';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAllContent =>
|
||||||
|
'Ste prepričani, da želite izbrisati vse odkrite kontakte?';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get common_delete => 'Radera';
|
String get common_delete => 'Radera';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get common_deleteAll => 'Ta bort alla';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get common_close => 'Stänga';
|
String get common_close => 'Stänga';
|
||||||
|
|
||||||
@@ -108,6 +111,89 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
String get scanner_title => 'MeshCore Open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceUsbLabel => 'USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Anslut via USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Välj en USB-enhet';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'USB-seriell kommunikation är aktiv på stödda Android-enheter och på skrivbordsplattformar.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'Inga USB-enheter hittades. Anslut en och uppdatera.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied => 'Tillgången via USB nekas.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing =>
|
||||||
|
'Den valda USB-enheten är inte längre tillgänglig.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Välj en giltig USB-enhet.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy =>
|
||||||
|
'En annan förfrågan om USB-anslutning är redan pågående.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'Ingen USB-enhet är ansluten.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed =>
|
||||||
|
'Misslyckades med att öppna det valda USB-enheten.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'Kunde inte ansluta till det valda USB-enheten.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'USB-seriell kommunikation stöds inte på denna plattform.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'En USB-anslutning är redan aktiv.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected => 'Ingen USB-enhet valdes.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'USB-anslutningen är inte aktiv.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName => 'Web-serieenhet';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Välj en USB-enhet';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Anslutning till USB-enhet...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Söker efter USB-enheter...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'Fel vid USB-anslutning: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Söker efter enheter...';
|
String get scanner_scanning => 'Söker efter enheter...';
|
||||||
|
|
||||||
@@ -149,6 +235,13 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
String get scanner_bluetoothOffMessage =>
|
String get scanner_bluetoothOffMessage =>
|
||||||
'Vänligen aktivera Bluetooth för att söka efter enheter.';
|
'Vänligen aktivera Bluetooth för att söka efter enheter.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequired => 'Chrome-webbläsare krävs';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequiredMessage =>
|
||||||
|
'Denna webbapplikation kräver Google Chrome oder en Chromium-baserader webbläsare för Bluetooth-stöd.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_enableBluetooth => 'Aktivera Bluetooth';
|
String get scanner_enableBluetooth => 'Aktivera Bluetooth';
|
||||||
|
|
||||||
@@ -232,6 +325,13 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_longitude => 'Längdgrad';
|
String get settings_longitude => 'Längdgrad';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettings => 'Kontaktinställningar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettingsSubtitle =>
|
||||||
|
'Inställningar för hur kontakter läggs till.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_privacyMode => 'Privatläge';
|
String get settings_privacyMode => 'Privatläge';
|
||||||
|
|
||||||
@@ -317,6 +417,10 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
String get settings_aboutDescription =>
|
String get settings_aboutDescription =>
|
||||||
'En öppen källkods Flutter-klient för MeshCore LoRa meshnätverksenheter.';
|
'En öppen källkods Flutter-klient för MeshCore LoRa meshnätverksenheter.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_aboutOpenMeteoAttribution =>
|
||||||
|
'LOS-höjddata: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_infoName => 'Namn';
|
String get settings_infoName => 'Namn';
|
||||||
|
|
||||||
@@ -453,6 +557,13 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageUk => 'Ukrainska';
|
String get appSettings_languageUk => 'Ukrainska';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracing => 'Aktivera meddelandespårning';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracingSubtitle =>
|
||||||
|
'Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Meddelanden';
|
String get appSettings_notifications => 'Meddelanden';
|
||||||
|
|
||||||
@@ -610,6 +721,15 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_offlineMapCache => 'Offline Kartcache';
|
String get appSettings_offlineMapCache => 'Offline Kartcache';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsTitle => 'Enheter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsMetric => 'Metriskt (m/km)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsImperial => 'Imperialt (ft / mi)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_noAreaSelected => 'Ingen area markerad';
|
String get appSettings_noAreaSelected => 'Ingen area markerad';
|
||||||
|
|
||||||
@@ -647,7 +767,35 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
'Kontakter kommer att visas när enheter annonserar.';
|
'Kontakter kommer att visas när enheter annonserar.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_searchContacts => 'Sök kontakter...';
|
String get contacts_unread => 'Oläst';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_searchContactsNoNumber => 'Sök kontakter...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchContacts(int number, String str) {
|
||||||
|
return 'Sök kontakter...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchFavorites(int number, String str) {
|
||||||
|
return 'Sök $number$str Favoriter...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchUsers(int number, String str) {
|
||||||
|
return 'Sök $number$str användare...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRepeaters(int number, String str) {
|
||||||
|
return 'Sök $number$str upprepningsenheter...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRoomServers(int number, String str) {
|
||||||
|
return 'Sök $number$str Room-servrar...';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_noUnreadContacts => 'Inga oinlästa kontakter';
|
String get contacts_noUnreadContacts => 'Inga oinlästa kontakter';
|
||||||
@@ -773,6 +921,12 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_editChannel => 'Redigera kanal';
|
String get channels_editChannel => 'Redigera kanal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_muteChannel => 'Tysta kanal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_unmuteChannel => 'Slå på ljud för kanal';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_deleteChannel => 'Ta bort kanal';
|
String get channels_deleteChannel => 'Ta bort kanal';
|
||||||
|
|
||||||
@@ -781,6 +935,11 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
return 'Radera \"$name\"? Detta kan inte ångras.';
|
return 'Radera \"$name\"? Detta kan inte ångras.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channels_channelDeleteFailed(String name) {
|
||||||
|
return 'Det gick inte att ta bort kanalen \"$name\"';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String channels_channelDeleted(String name) {
|
String channels_channelDeleted(String name) {
|
||||||
return 'Kanalen \"$name\" raderad';
|
return 'Kanalen \"$name\" raderad';
|
||||||
@@ -1069,6 +1228,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_pathManagement => 'Stigarhantering';
|
String get chat_pathManagement => 'Stigarhantering';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_ShowAllPaths => 'Visa alla vägar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_routingMode => 'Ruttläge';
|
String get chat_routingMode => 'Ruttläge';
|
||||||
|
|
||||||
@@ -1225,6 +1387,12 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_title => 'Nodkarta';
|
String get map_title => 'Nodkarta';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_lineOfSight => 'Synlinje';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_losScreenTitle => 'Synlinje';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_noNodesWithLocation => 'Inga noder med platsinformation';
|
String get map_noNodesWithLocation => 'Inga noder med platsinformation';
|
||||||
|
|
||||||
@@ -1342,6 +1510,13 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_showSharedMarkers => 'Visa delade markörer';
|
String get map_showSharedMarkers => 'Visa delade markörer';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showGuessedLocations =>
|
||||||
|
'Visa upp de antagna nodernas placeringar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_guessedLocation => 'Gissad plats';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_lastSeenTime => 'Senaste Visats Tid';
|
String get map_lastSeenTime => 'Senaste Visats Tid';
|
||||||
|
|
||||||
@@ -1658,10 +1833,10 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Skicka kommandon till repetitorn';
|
String get repeater_cliSubtitle => 'Skicka kommandon till repetitorn';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Grannar';
|
String get repeater_neighbors => 'Grannar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle => 'Visa noll hoppgrannar.';
|
String get repeater_neighborsSubtitle => 'Visa noll hoppgrannar.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => 'Inställningar';
|
String get repeater_settings => 'Inställningar';
|
||||||
@@ -2352,7 +2527,7 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Upprepar grannar';
|
String get neighbors_repeatersNeighbors => 'Upprepar grannar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Inga grannuppgifter finns tillgängliga.';
|
String get neighbors_noData => 'Inga grannuppgifter finns tillgängliga.';
|
||||||
@@ -2660,6 +2835,15 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get listFilter_all => 'Alla';
|
String get listFilter_all => 'Alla';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_favorites => 'Favoriter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_addToFavorites => 'Lägg till i favoriter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_removeFromFavorites => 'Ta bort från favoriter';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_users => 'Användare';
|
String get listFilter_users => 'Användare';
|
||||||
|
|
||||||
@@ -2694,6 +2878,142 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get pathTrace_clearTooltip => 'Rensa väg';
|
String get pathTrace_clearTooltip => 'Rensa väg';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectStartEnd => 'Välj start- och slutnoder för LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losRunFailed(String error) {
|
||||||
|
return 'Synlinjekontroll misslyckades: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losClearAllPoints => 'Rensa alla punkter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRunToViewElevationProfile => 'Kör LOS för att se höjdprofil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuTitle => 'LOS-menyn';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuSubtitle =>
|
||||||
|
'Tryck på noder eller tryck länge på kartan för anpassade punkter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowDisplayNodes => 'Visa displaynoder';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losCustomPoints => 'Anpassade poäng';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losCustomPointLabel(int index) {
|
||||||
|
return 'Anpassad $index';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointA => 'Punkt A';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointB => 'Punkt B';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaA(String value, String unit) {
|
||||||
|
return 'Antenn A: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaB(String value, String unit) {
|
||||||
|
return 'Antenn B: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRun => 'Kör LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losNoElevationData => 'Inga höjddata';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileClear(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String clearance,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, rensa LOS, min clearance $clearance $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileBlocked(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, blockerad av $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusChecking => 'LOS: kollar...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusNoData => 'LOS: inga data';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||||
|
return 'LOS: $clear/$total rensa, $blocked blockerad, $unknown okänd';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorElevationUnavailable =>
|
||||||
|
'Höjddata är inte tillgänglig för ett eller flera prover.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorInvalidInput =>
|
||||||
|
'Ogiltiga poäng/höjddata för LOS-beräkning.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRenameCustomPoint => 'Byt namn på anpassad punkt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointName => 'Punktnamn';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowPanelTooltip => 'Visa LOS-panelen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losHidePanelTooltip => 'Dölj LOS-panelen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losElevationAttribution => 'Höjddata: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendRadioHorizon => 'Radiohorisont';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendLosBeam => 'Siktlinje';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendTerrain => 'Terräng';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyLabel => 'Frekvens';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyInfoTooltip => 'Visa detaljer om beräkningen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyDialogTitle => 'Beräkning av radiohorisonten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losFrequencyDialogDescription(
|
||||||
|
double baselineK,
|
||||||
|
double baselineFreq,
|
||||||
|
double frequencyMHz,
|
||||||
|
double kFactor,
|
||||||
|
) {
|
||||||
|
return 'Med start från k=$baselineK vid $baselineFreq MHz, justerar beräkningen k-faktorn för det aktuella $frequencyMHz MHz-bandet, som definierar den böjda radiohorisonten.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_pathTrace => 'Path Trace';
|
String get contacts_pathTrace => 'Path Trace';
|
||||||
|
|
||||||
@@ -2862,4 +3182,88 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open export av GPX-kartdata';
|
'meshcore-open export av GPX-kartdata';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Närliggande uppreparstationer';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Senast sedd';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_title => 'Kontaktinställningar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddTitle => 'Automatisk upptäckt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_otherTitle =>
|
||||||
|
'Andra inställningar relaterade till kontakt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersTitle =>
|
||||||
|
'Lägg till användare automatiskt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersSubtitle =>
|
||||||
|
'Tillåt kompanjonen att automatiskt lägga till upptäckta användare';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersTitle =>
|
||||||
|
'Lägg till upprepande enheter automatiskt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||||
|
'Tillåt kompanjonen att automatiskt lägga till upptäckta repeater.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersTitle =>
|
||||||
|
'Lägg automatiskt till rumsservrar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||||
|
'Tillåt kompanjonen att automatiskt lägga till upptäckta rumsservrar.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsTitle =>
|
||||||
|
'Lägg till sensorer automatiskt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||||
|
'Tillåt kompanjonen att automatiskt lägga till upptäckta sensorer.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestTitle => 'Skriv över äldst';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestSubtitle =>
|
||||||
|
'När kontaktlistan är full ersätts den äldsta icke-favoriterade kontakten.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_Title => 'Upptäckta kontakter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_noMatching => 'Inga matchande kontakter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_searchHint => 'Sök uppfunna kontakter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_contactAdded => 'Kontakt tillagd';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_addContact => 'Lägg till kontakt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_copyContact => 'Kopiera kontakt till urklipp';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContact => 'Ta bort kontakt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAll =>
|
||||||
|
'Ta bort alla upptäckta kontakter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAllContent =>
|
||||||
|
'Är du säker på att du vill ta bort alla upptäckta kontakter?';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get common_delete => 'Видалити';
|
String get common_delete => 'Видалити';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get common_deleteAll => 'Видалити все';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get common_close => 'Закрити';
|
String get common_close => 'Закрити';
|
||||||
|
|
||||||
@@ -108,6 +111,90 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => 'MeshCore Open';
|
String get scanner_title => 'MeshCore Open';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceUsbLabel => 'USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Підключити через USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Виберіть виявлене серійне пристрій і підключіть його безпосередньо до вашого вузла MeshCore.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Виберіть пристрій USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'USB-серіальний інтерфейс активний на підтримуваних пристроях на базі Android та на десктопних платформах.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'Не знайдено жодних пристроїв USB. Підключіть один і перезавантажте.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied =>
|
||||||
|
'Було відмовлено у наданні дозволу на використання USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing => 'Вибране USB-пристрій більше недоступне.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Виберіть дійсний USB-пристрій.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy =>
|
||||||
|
'Ще один запит на підключення через USB вже обробляється.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'Немає підключених пристроїв USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed => 'Не вдалося відкрити вибране USB-пристрій.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'Не вдалося підключитися до вибраного USB-пристрою.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'Підтримка USB-серіального інтерфейсу не реалізована на цій платформі.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'USB-з\'єднання вже встановлено.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected =>
|
||||||
|
'Не було вибрано жодного пристрою USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'З\'єднання USB не встановлено.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'З\'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName =>
|
||||||
|
'Пристрій для передачі даних по веб-серіалах';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Виберіть пристрій USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Підключення до USB-пристрою...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Пошук пристроїв USB...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'Не вдалося встановити з\'єднання через USB: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Пошук пристроїв...';
|
String get scanner_scanning => 'Пошук пристроїв...';
|
||||||
|
|
||||||
@@ -150,6 +237,13 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
String get scanner_bluetoothOffMessage =>
|
String get scanner_bluetoothOffMessage =>
|
||||||
'Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.';
|
'Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequired => 'Потрібен браузер Chrome';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequiredMessage =>
|
||||||
|
'Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_enableBluetooth => 'Увімкніть Bluetooth';
|
String get scanner_enableBluetooth => 'Увімкніть Bluetooth';
|
||||||
|
|
||||||
@@ -232,6 +326,13 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_longitude => 'Довгота';
|
String get settings_longitude => 'Довгота';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettings => 'Налаштування контактів';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_contactSettingsSubtitle =>
|
||||||
|
'Налаштування для додавання контактів';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_privacyMode => 'Режим приватності';
|
String get settings_privacyMode => 'Режим приватності';
|
||||||
|
|
||||||
@@ -322,6 +423,10 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
String get settings_aboutDescription =>
|
String get settings_aboutDescription =>
|
||||||
'Клієнт Flutter з відкритим вихідним кодом для пристроїв мережі MeshCore LoRa.';
|
'Клієнт Flutter з відкритим вихідним кодом для пристроїв мережі MeshCore LoRa.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_aboutOpenMeteoAttribution =>
|
||||||
|
'Дані про висоту LOS: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_infoName => 'Ім\'я';
|
String get settings_infoName => 'Ім\'я';
|
||||||
|
|
||||||
@@ -458,6 +563,14 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageUk => 'Українська';
|
String get appSettings_languageUk => 'Українська';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracing =>
|
||||||
|
'Увімкнути відстеження повідомлень';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracingSubtitle =>
|
||||||
|
'Показувати детальні метадані про маршрутизацію та час для повідомлень';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Сповіщення';
|
String get appSettings_notifications => 'Сповіщення';
|
||||||
|
|
||||||
@@ -618,6 +731,15 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_offlineMapCache => 'Офлайн-кеш карти';
|
String get appSettings_offlineMapCache => 'Офлайн-кеш карти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsTitle => 'одиниці';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsMetric => 'Метричний (м / км)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsImperial => 'Імперська (ft / mi)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_noAreaSelected => 'Область не вибрано';
|
String get appSettings_noAreaSelected => 'Область не вибрано';
|
||||||
|
|
||||||
@@ -655,7 +777,35 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
'Контакти з\'являться, коли пристрої надішлють оголошення.';
|
'Контакти з\'являться, коли пристрої надішлють оголошення.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_searchContacts => 'Пошук контактів...';
|
String get contacts_unread => 'Непрочитане';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_searchContactsNoNumber => 'Пошук контактів...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchContacts(int number, String str) {
|
||||||
|
return 'Пошук контактів...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchFavorites(int number, String str) {
|
||||||
|
return 'Пошук $number$str улюблених...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchUsers(int number, String str) {
|
||||||
|
return 'Пошук $number$str користувачів...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRepeaters(int number, String str) {
|
||||||
|
return 'Пошук $number$str ретрансляторів...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRoomServers(int number, String str) {
|
||||||
|
return 'Пошук $number$str серверів кімнат...';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_noUnreadContacts => 'Немає непрочитаних контактів';
|
String get contacts_noUnreadContacts => 'Немає непрочитаних контактів';
|
||||||
@@ -780,6 +930,12 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_editChannel => 'Редагувати канал';
|
String get channels_editChannel => 'Редагувати канал';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_muteChannel => 'Вимкнути сповіщення каналу';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_unmuteChannel => 'Увімкнути сповіщення каналу';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_deleteChannel => 'Видалити канал';
|
String get channels_deleteChannel => 'Видалити канал';
|
||||||
|
|
||||||
@@ -788,6 +944,11 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
return 'Видалити $name? Це не можна скасувати.';
|
return 'Видалити $name? Це не можна скасувати.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channels_channelDeleteFailed(String name) {
|
||||||
|
return 'Не вдалося видалити канал \"$name\"';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String channels_channelDeleted(String name) {
|
String channels_channelDeleted(String name) {
|
||||||
return 'Канал «$name» видалено';
|
return 'Канал «$name» видалено';
|
||||||
@@ -1075,6 +1236,9 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_pathManagement => 'Керування шляхами';
|
String get chat_pathManagement => 'Керування шляхами';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_ShowAllPaths => 'Показати всі шляхи';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_routingMode => 'Режим маршрутизації';
|
String get chat_routingMode => 'Режим маршрутизації';
|
||||||
|
|
||||||
@@ -1237,6 +1401,12 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_title => 'Карта вузлів';
|
String get map_title => 'Карта вузлів';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_lineOfSight => 'Пряма видимість';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_losScreenTitle => 'Пряма видимість';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_noNodesWithLocation =>
|
String get map_noNodesWithLocation =>
|
||||||
'Немає вузлів з даними про розташування';
|
'Немає вузлів з даними про розташування';
|
||||||
@@ -1355,6 +1525,13 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_showSharedMarkers => 'Показувати спільні маркери';
|
String get map_showSharedMarkers => 'Показувати спільні маркери';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showGuessedLocations =>
|
||||||
|
'Показати місцезнаходження передбачених вузлів';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_guessedLocation => 'Визначено місцезнаходження';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_lastSeenTime => 'Час останньої активності';
|
String get map_lastSeenTime => 'Час останньої активності';
|
||||||
|
|
||||||
@@ -1675,10 +1852,10 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Надіслати команди ретранслятору';
|
String get repeater_cliSubtitle => 'Надіслати команди ретранслятору';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Сусіди';
|
String get repeater_neighbors => 'Сусіди';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle =>
|
String get repeater_neighborsSubtitle =>
|
||||||
'Показати сусідів нульового стрибка.';
|
'Показати сусідів нульового стрибка.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -2380,7 +2557,7 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Ретранслятори-сусіди';
|
String get neighbors_repeatersNeighbors => 'Ретранслятори-сусіди';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Дані про сусідів недоступні.';
|
String get neighbors_noData => 'Дані про сусідів недоступні.';
|
||||||
@@ -2696,6 +2873,15 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get listFilter_all => 'Все';
|
String get listFilter_all => 'Все';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_favorites => 'Улюблені';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_addToFavorites => 'Додати до улюблених';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_removeFromFavorites => 'Видалити зі списку улюблених';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_users => 'Користувачі';
|
String get listFilter_users => 'Користувачі';
|
||||||
|
|
||||||
@@ -2730,6 +2916,145 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get pathTrace_clearTooltip => 'Очистити шлях';
|
String get pathTrace_clearTooltip => 'Очистити шлях';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectStartEnd =>
|
||||||
|
'Виберіть початковий і кінцевий вузли для LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losRunFailed(String error) {
|
||||||
|
return 'Помилка перевірки прямої видимості: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losClearAllPoints => 'Очистити всі пункти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRunToViewElevationProfile =>
|
||||||
|
'Запустіть LOS, щоб переглянути профіль висоти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuTitle => 'Меню LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuSubtitle =>
|
||||||
|
'Торкніться вузлів або утримуйте карту, щоб отримати власні точки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowDisplayNodes => 'Показати вузли відображення';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losCustomPoints => 'Користувальницькі точки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losCustomPointLabel(int index) {
|
||||||
|
return 'Спеціальний $index';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointA => 'Точка А';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointB => 'Точка Б';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaA(String value, String unit) {
|
||||||
|
return 'Антена A: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaB(String value, String unit) {
|
||||||
|
return 'Антена B: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRun => 'Запустіть LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losNoElevationData => 'Немає даних про висоту';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileClear(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String clearance,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, чистий LOS, мінімальний зазор $clearance $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileBlocked(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, заблоковано $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusChecking => 'LOS: перевірка...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusNoData => 'LOS: немає даних';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||||
|
return 'LOS: $clear/$total очищено, $blocked заблоковано, $unknown невідомо';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorElevationUnavailable =>
|
||||||
|
'Дані про висоту недоступні для одного чи кількох зразків.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorInvalidInput =>
|
||||||
|
'Недійсні дані про точки/висоту для розрахунку LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRenameCustomPoint => 'Перейменуйте спеціальну точку';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointName => 'Назва точки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowPanelTooltip => 'Показати панель LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losHidePanelTooltip => 'Приховати панель LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losElevationAttribution =>
|
||||||
|
'Дані про висоту: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendRadioHorizon => 'Радіогоризонт';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendLosBeam => 'Лінія прямої видимості';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendTerrain => 'Рельєф';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyLabel => 'Частота';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyInfoTooltip => 'Переглянути деталі розрахунку';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyDialogTitle => 'Розрахунок радіогоризонту';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losFrequencyDialogDescription(
|
||||||
|
double baselineK,
|
||||||
|
double baselineFreq,
|
||||||
|
double frequencyMHz,
|
||||||
|
double kFactor,
|
||||||
|
) {
|
||||||
|
return 'Починаючи з k=$baselineK на $baselineFreq МГц, обчислення коригує k-фактор для поточного діапазону $frequencyMHz МГц, який визначає викривлену межу радіогоризонту.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_pathTrace => 'Трасування шляхів';
|
String get contacts_pathTrace => 'Трасування шляхів';
|
||||||
|
|
||||||
@@ -2907,4 +3232,90 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'експорт даних карти meshcore-open у форматі GPX';
|
'експорт даних карти meshcore-open у форматі GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Ближні ретранслятори';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Останній раз бачили';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_title => 'Налаштування контактів';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddTitle => 'Автоматичне виявлення';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_otherTitle =>
|
||||||
|
'Інші налаштування, пов\'язані з контактами';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersTitle =>
|
||||||
|
'Автоматично додавати користувачів';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersSubtitle =>
|
||||||
|
'Дозволити супутникові автоматично додавати виявлених користувачів';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersTitle =>
|
||||||
|
'Автоматично додавати повторювачі';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||||
|
'Дозволити супутнику автоматично додавати виявлені ретранслятори';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersTitle =>
|
||||||
|
'Автоматично додавати сервери кімнат';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||||
|
'Дозволити супровіднику автоматично додавати виявлені сервери кімнат.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsTitle =>
|
||||||
|
'Автоматично додавати датчики';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||||
|
'Дозволити супровіднику автоматично додавати виявлені сенсори';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestTitle => 'Перезаписати найстаріше';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestSubtitle =>
|
||||||
|
'Коли список контактів заповнений, найстарший контакт без позначки улюбленого буде замінений.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_Title => 'Виявлені контакти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_noMatching =>
|
||||||
|
'Відповідних контактів не знайдено';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_searchHint => 'Знайти виявлені контакти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_contactAdded => 'Контакт додано';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_addContact => 'Додати контакт';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_copyContact =>
|
||||||
|
'Копіювати контакт у буфер обміну';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContact => 'Видалити контакт';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAll =>
|
||||||
|
'Видалити всі виявлені контакти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAllContent =>
|
||||||
|
'Ви впевнені, що хочете видалити всі виявлені контакти?';
|
||||||
}
|
}
|
||||||
|
|||||||
+877
-538
File diff suppressed because it is too large
Load Diff
+267
-4
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"channels_channelDeleteFailed": "Kan kanaal {name} niet verwijderen",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@@locale": "nl",
|
"@@locale": "nl",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Contacten",
|
"nav_contacts": "Contacten",
|
||||||
@@ -334,6 +342,8 @@
|
|||||||
"channels_publicChannel": "Open kanaal",
|
"channels_publicChannel": "Open kanaal",
|
||||||
"channels_privateChannel": "Private kanaal",
|
"channels_privateChannel": "Private kanaal",
|
||||||
"channels_editChannel": "Kanaal bewerken",
|
"channels_editChannel": "Kanaal bewerken",
|
||||||
|
"channels_muteChannel": "Kanaal dempen",
|
||||||
|
"channels_unmuteChannel": "Kanaal dempen opheffen",
|
||||||
"channels_deleteChannel": "Kanaal verwijderen",
|
"channels_deleteChannel": "Kanaal verwijderen",
|
||||||
"channels_deleteChannelConfirm": "Verwijderen \"{name}\"? Dit kan niet worden teruggedraaid.",
|
"channels_deleteChannelConfirm": "Verwijderen \"{name}\"? Dit kan niet worden teruggedraaid.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
@@ -1351,12 +1361,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Buren",
|
"repeater_neighbors": "Buren",
|
||||||
"repeater_neighboursSubtitle": "Bekijk nul hops buren.",
|
"repeater_neighborsSubtitle": "Bekijk nul hops buren.",
|
||||||
"neighbors_receivedData": "Ontvangen Buurdata",
|
"neighbors_receivedData": "Ontvangen Buurdata",
|
||||||
"neighbors_requestTimedOut": "Buren vragen om tijdelijk uitgeschakeld.",
|
"neighbors_requestTimedOut": "Buren vragen om tijdelijk uitgeschakeld.",
|
||||||
"neighbors_errorLoading": "Fout bij het laden van buren: {error}",
|
"neighbors_errorLoading": "Fout bij het laden van buren: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Herhalingen Buren",
|
"neighbors_repeatersNeighbors": "Herhalingen Buren",
|
||||||
"neighbors_noData": "Geen gegevens van buren beschikbaar.",
|
"neighbors_noData": "Geen gegevens van buren beschikbaar.",
|
||||||
"channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.",
|
"channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.",
|
||||||
"channels_createPrivateChannel": "Maak een Privé Kanaal",
|
"channels_createPrivateChannel": "Maak een Privé Kanaal",
|
||||||
@@ -1555,6 +1565,8 @@
|
|||||||
"contacts_floodAdvert": "Overstromingsadvertentie",
|
"contacts_floodAdvert": "Overstromingsadvertentie",
|
||||||
"contacts_copyAdvertToClipboard": "Advert naar klembord kopiëren",
|
"contacts_copyAdvertToClipboard": "Advert naar klembord kopiëren",
|
||||||
"appSettings_languageRu": "Russisch",
|
"appSettings_languageRu": "Russisch",
|
||||||
|
"appSettings_enableMessageTracing": "Berichttracking inschakelen",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Gedetailleerde routerings- en timing-metadata voor berichten weergeven",
|
||||||
"contacts_clipboardEmpty": "Knipbord is leeg.",
|
"contacts_clipboardEmpty": "Knipbord is leeg.",
|
||||||
"contacts_addContactFromClipboard": "Contact uit klembord toevoegen",
|
"contacts_addContactFromClipboard": "Contact uit klembord toevoegen",
|
||||||
"contacts_contactImported": "Contact is geïmporteerd.",
|
"contacts_contactImported": "Contact is geïmporteerd.",
|
||||||
@@ -1593,8 +1605,259 @@
|
|||||||
"map_runTrace": "Padeshulp traceren",
|
"map_runTrace": "Padeshulp traceren",
|
||||||
"scanner_enableBluetooth": "Activeer Bluetooth",
|
"scanner_enableBluetooth": "Activeer Bluetooth",
|
||||||
"scanner_bluetoothOffMessage": "Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.",
|
"scanner_bluetoothOffMessage": "Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.",
|
||||||
|
"scanner_chromeRequired": "Chrome-browser vereist",
|
||||||
|
"scanner_chromeRequiredMessage": "Deze webapplicatie vereist Google Chrome of een op Chromium gebaseerde browser voor Bluetooth-ondersteuning.",
|
||||||
"scanner_bluetoothOff": "Bluetooth is uitgeschakeld",
|
"scanner_bluetoothOff": "Bluetooth is uitgeschakeld",
|
||||||
|
"snrIndicator_lastSeen": "Laatst gezien",
|
||||||
|
"snrIndicator_nearByRepeaters": "Nabije herhalingseenheden",
|
||||||
|
"chat_ShowAllPaths": "Toon alle paden",
|
||||||
"settings_clientRepeat": "Herhalen: Afgekoppeld",
|
"settings_clientRepeat": "Herhalen: Afgekoppeld",
|
||||||
"settings_clientRepeatSubtitle": "Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.",
|
"settings_clientRepeatSubtitle": "Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.",
|
||||||
"settings_clientRepeatFreqWarning": "Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist."
|
"settings_clientRepeatFreqWarning": "Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist.",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "LOS-hoogtegegevens: Open-Meteo (CC BY 4.0)",
|
||||||
|
"appSettings_unitsTitle": "Eenheden",
|
||||||
|
"appSettings_unitsMetric": "Metrisch (m / km)",
|
||||||
|
"appSettings_unitsImperial": "Imperiaal (ft / mi)",
|
||||||
|
"map_lineOfSight": "Zichtlijn",
|
||||||
|
"map_losScreenTitle": "Zichtlijn",
|
||||||
|
"losSelectStartEnd": "Selecteer begin- en eindknooppunten voor LOS.",
|
||||||
|
"losRunFailed": "Zichtlijncontrole mislukt: {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Wis alle punten",
|
||||||
|
"losRunToViewElevationProfile": "Voer LOS uit om het hoogteprofiel te bekijken",
|
||||||
|
"losMenuTitle": "LOS-menu",
|
||||||
|
"losMenuSubtitle": "Tik op knooppunten of druk lang op de kaart voor aangepaste punten",
|
||||||
|
"losShowDisplayNodes": "Toon weergaveknooppunten",
|
||||||
|
"losCustomPoints": "Aangepaste punten",
|
||||||
|
"losCustomPointLabel": "Aangepast {index}",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Punt A",
|
||||||
|
"losPointB": "Punt B",
|
||||||
|
"losAntennaA": "Antenne A: {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Antenne B: {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Voer LOS uit",
|
||||||
|
"losNoElevationData": "Geen hoogtegegevens",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, vrije LOS, min. vrije ruimte {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, geblokkeerd door {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "LOS: controleren...",
|
||||||
|
"losStatusNoData": "LOS: geen gegevens",
|
||||||
|
"losStatusSummary": "LOS: {clear}/{total} gewist, {blocked} geblokkeerd, {unknown} onbekend",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Hoogtegegevens niet beschikbaar voor een of meer monsters.",
|
||||||
|
"losErrorInvalidInput": "Ongeldige punten/hoogtegegevens voor LOS-berekening.",
|
||||||
|
"losRenameCustomPoint": "Hernoem aangepast punt",
|
||||||
|
"losPointName": "Puntnaam",
|
||||||
|
"losShowPanelTooltip": "Toon LOS-paneel",
|
||||||
|
"losHidePanelTooltip": "LOS-paneel verbergen",
|
||||||
|
"losElevationAttribution": "Hoogtegegevens: Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Radiohorizon",
|
||||||
|
"losLegendLosBeam": "Zichtlijn",
|
||||||
|
"losLegendTerrain": "Terrein",
|
||||||
|
"losFrequencyLabel": "Frequentie",
|
||||||
|
"losFrequencyInfoTooltip": "Bekijk details van de berekening",
|
||||||
|
"losFrequencyDialogTitle": "Berekening van de radiohorizon",
|
||||||
|
"losFrequencyDialogDescription": "Beginnend met k={baselineK} bij {baselineFreq} MHz, wordt bij de berekening de k-factor aangepast voor de huidige {frequencyMHz} MHz-band, die de gebogen radiohorizonkap definieert.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listFilter_removeFromFavorites": "Verwijderen uit favorieten",
|
||||||
|
"listFilter_favorites": "Favorieten",
|
||||||
|
"listFilter_addToFavorites": "Toevoegen aan favorieten",
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_unread": "Ongelezen",
|
||||||
|
"contacts_searchRepeaters": "Zoek {number}{str} Repeaters...",
|
||||||
|
"contacts_searchContactsNoNumber": "Zoek contacten...",
|
||||||
|
"contacts_searchUsers": "Zoek {number}{str} gebruikers...",
|
||||||
|
"contacts_searchFavorites": "Zoek {number}{str} favorieten...",
|
||||||
|
"contacts_searchRoomServers": "Zoek {number}{str} Room servers...",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Gebruikers automatisch toevoegen",
|
||||||
|
"contactsSettings_title": "Instellingen voor contacten",
|
||||||
|
"settings_contactSettings": "Contactinstellingen",
|
||||||
|
"contactsSettings_otherTitle": "Andere instellingen voor contactgerelateerde zaken",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Sta toe dat de companion automatisch ontdekte repeaters toevoegt",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Automatisch kamerservers toevoegen",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Sta toe dat de companion automatisch ontdekte kamer servers toevoegt.",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Automatisch sensoren toevoegen",
|
||||||
|
"settings_contactSettingsSubtitle": "Instellingen voor het toevoegen van contacten",
|
||||||
|
"contactsSettings_autoAddTitle": "Automatische detectie",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Sta toe dat de companion automatisch ontdekte sensoren toevoegt",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Sta toe dat de companion automatisch ontdekte gebruikers toevoegt",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Automatisch herhalingstoestellen toevoegen",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Overschrijf Oudste",
|
||||||
|
"discoveredContacts_noMatching": "Geen overeenkomende contacten",
|
||||||
|
"discoveredContacts_addContact": "Contact toevoegen",
|
||||||
|
"discoveredContacts_copyContact": "Kopieer contact naar klembord",
|
||||||
|
"discoveredContacts_deleteContact": "Contact verwijderen",
|
||||||
|
"discoveredContacts_Title": "Ontdekte contacten",
|
||||||
|
"discoveredContacts_contactAdded": "Contact toegevoegd",
|
||||||
|
"discoveredContacts_searchHint": "Ontdekte contacten zoeken",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Wanneer de contactenlijst vol is, wordt de oudste niet-favoriete contactpersoon vervangen.",
|
||||||
|
"common_deleteAll": "Alles verwijderen",
|
||||||
|
"discoveredContacts_deleteContactAll": "Verwijder alle ontdekte contacten",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Weet u zeker dat u alle ontdekte contacten wilt verwijderen?",
|
||||||
|
"map_guessedLocation": "Geroerde locatie",
|
||||||
|
"map_showGuessedLocations": "Toon de voorspelde locaties van de knopen",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenSubtitle": "Selecteer een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenTitle": "Verbind via USB",
|
||||||
|
"usbScreenStatus": "Selecteer een USB-apparaat",
|
||||||
|
"usbScreenNote": "USB-serieel is actief op ondersteunde Android-apparaten en desktop-platforms.",
|
||||||
|
"usbScreenEmptyState": "Geen USB-apparaten gevonden. Sluit er een aan en herlaad.",
|
||||||
|
"usbErrorPermissionDenied": "Toegang via USB is geweigerd.",
|
||||||
|
"usbErrorDeviceMissing": "Het geselecteerde USB-apparaat is niet meer beschikbaar.",
|
||||||
|
"usbErrorInvalidPort": "Selecteer een geldig USB-apparaat.",
|
||||||
|
"usbErrorBusy": "Een andere verzoek om een USB-verbinding is al in behandeling.",
|
||||||
|
"usbErrorNotConnected": "Er is geen USB-apparaat aangesloten.",
|
||||||
|
"usbErrorOpenFailed": "Kon het geselecteerde USB-apparaat niet openen.",
|
||||||
|
"usbErrorConnectFailed": "Kon niet verbinding maken met het geselecteerde USB-apparaat.",
|
||||||
|
"usbErrorUnsupported": "USB-serieel is niet ondersteund op deze platform.",
|
||||||
|
"usbErrorAlreadyActive": "Een USB-verbinding is al actief.",
|
||||||
|
"usbErrorNoDeviceSelected": "Geen USB-apparaat is geselecteerd.",
|
||||||
|
"usbErrorPortClosed": "De USB-verbinding is niet actief.",
|
||||||
|
"usbFallbackDeviceName": "Web-serieapparaat",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbConnectionFailed": "Fout bij de USB-verbinding: {error}",
|
||||||
|
"usbStatus_notConnected": "Selecteer een USB-apparaat",
|
||||||
|
"usbStatus_connecting": "Verbinding maken met USB-apparaat...",
|
||||||
|
"usbStatus_searching": "Zoeken naar USB-apparaten...",
|
||||||
|
"usbErrorConnectTimedOut": "Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft."
|
||||||
}
|
}
|
||||||
|
|||||||
+267
-4
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"channels_channelDeleteFailed": "Nie udało się usunąć kanału \"{name}\"",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@@locale": "pl",
|
"@@locale": "pl",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Kontakty",
|
"nav_contacts": "Kontakty",
|
||||||
@@ -334,6 +342,8 @@
|
|||||||
"channels_publicChannel": "Kanał publiczny",
|
"channels_publicChannel": "Kanał publiczny",
|
||||||
"channels_privateChannel": "Prywatny kanał",
|
"channels_privateChannel": "Prywatny kanał",
|
||||||
"channels_editChannel": "Edytuj kanał",
|
"channels_editChannel": "Edytuj kanał",
|
||||||
|
"channels_muteChannel": "Wycisz kanał",
|
||||||
|
"channels_unmuteChannel": "Wyłącz wyciszenie kanału",
|
||||||
"channels_deleteChannel": "Usuń kanał",
|
"channels_deleteChannel": "Usuń kanał",
|
||||||
"channels_deleteChannelConfirm": "Usuń \"{name}\"? Nie można tego cofnąć.",
|
"channels_deleteChannelConfirm": "Usuń \"{name}\"? Nie można tego cofnąć.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
@@ -1351,12 +1361,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Sąsiedzi",
|
"repeater_neighbors": "Sąsiedzi",
|
||||||
"repeater_neighboursSubtitle": "Wyświetl sąsiedztwo zerowych hopów.",
|
"repeater_neighborsSubtitle": "Wyświetl sąsiedztwo zerowych hopów.",
|
||||||
"neighbors_receivedData": "Otrzymano dane sąsiedztwa",
|
"neighbors_receivedData": "Otrzymano dane sąsiedztwa",
|
||||||
"neighbors_requestTimedOut": "Sąsiedzi proszą o wyłączenie timingu.",
|
"neighbors_requestTimedOut": "Sąsiedzi proszą o wyłączenie timingu.",
|
||||||
"neighbors_errorLoading": "Błąd podczas ładowania sąsiadów: {error}",
|
"neighbors_errorLoading": "Błąd podczas ładowania sąsiadów: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Powtarzacze Sąsiedzi",
|
"neighbors_repeatersNeighbors": "Powtarzacze Sąsiedzi",
|
||||||
"neighbors_noData": "Brak danych dotyczących sąsiadów.",
|
"neighbors_noData": "Brak danych dotyczących sąsiadów.",
|
||||||
"channels_joinPrivateChannelDesc": "Ręcznie wprowadź klucz tajny.",
|
"channels_joinPrivateChannelDesc": "Ręcznie wprowadź klucz tajny.",
|
||||||
"channels_createPrivateChannel": "Utwórz Prywatny Kanał",
|
"channels_createPrivateChannel": "Utwórz Prywatny Kanał",
|
||||||
@@ -1550,6 +1560,8 @@
|
|||||||
"contacts_chatTraceRoute": "Śledź trasę promienia",
|
"contacts_chatTraceRoute": "Śledź trasę promienia",
|
||||||
"appSettings_languageRu": "Rosyjski",
|
"appSettings_languageRu": "Rosyjski",
|
||||||
"appSettings_languageUk": "Ukraińska",
|
"appSettings_languageUk": "Ukraińska",
|
||||||
|
"appSettings_enableMessageTracing": "Włącz śledzenie wiadomości",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Pokaż szczegółowe metadane trasowania i czasu dla wiadomości",
|
||||||
"contacts_contactImportFailed": "Kontakt nie został zaimportowany.",
|
"contacts_contactImportFailed": "Kontakt nie został zaimportowany.",
|
||||||
"contacts_zeroHopAdvert": "Reklama Zero Hop",
|
"contacts_zeroHopAdvert": "Reklama Zero Hop",
|
||||||
"contacts_floodAdvert": "Reklama powodziowa",
|
"contacts_floodAdvert": "Reklama powodziowa",
|
||||||
@@ -1592,9 +1604,260 @@
|
|||||||
"map_removeLast": "Usuń ostatni",
|
"map_removeLast": "Usuń ostatni",
|
||||||
"map_tapToAdd": "Kliknij na węzły, aby dodać je do ścieżki.",
|
"map_tapToAdd": "Kliknij na węzły, aby dodać je do ścieżki.",
|
||||||
"scanner_bluetoothOffMessage": "Prosimy włączyć Bluetooth, aby przeskanować urządzenia.",
|
"scanner_bluetoothOffMessage": "Prosimy włączyć Bluetooth, aby przeskanować urządzenia.",
|
||||||
|
"scanner_chromeRequired": "Wymagana przeglądarka Chrome",
|
||||||
|
"scanner_chromeRequiredMessage": "Ta aplikacja internetowa wymaga przeglądarki Google Chrome lub opartej na Chromium do obsługi Bluetooth.",
|
||||||
"scanner_bluetoothOff": "Bluetooth jest wyłączony",
|
"scanner_bluetoothOff": "Bluetooth jest wyłączony",
|
||||||
"scanner_enableBluetooth": "Włącz Bluetooth",
|
"scanner_enableBluetooth": "Włącz Bluetooth",
|
||||||
|
"snrIndicator_lastSeen": "Ostatnio widziany",
|
||||||
|
"snrIndicator_nearByRepeaters": "Nadajniki w pobliżu",
|
||||||
|
"chat_ShowAllPaths": "Pokaż wszystkie ścieżki",
|
||||||
"settings_clientRepeatSubtitle": "Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.",
|
"settings_clientRepeatSubtitle": "Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.",
|
||||||
"settings_clientRepeat": "Powtórzenie: Niezależne od sieci",
|
"settings_clientRepeat": "Powtórzenie: Niezależne od sieci",
|
||||||
"settings_clientRepeatFreqWarning": "Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz."
|
"settings_clientRepeatFreqWarning": "Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz.",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "Dane wysokościowe LOS: Open-Meteo (CC BY 4.0)",
|
||||||
|
"appSettings_unitsTitle": "Jednostki",
|
||||||
|
"appSettings_unitsMetric": "Metryczne (m / km)",
|
||||||
|
"appSettings_unitsImperial": "Imperialne (ft / mi)",
|
||||||
|
"map_lineOfSight": "Linia wzroku",
|
||||||
|
"map_losScreenTitle": "Linia wzroku",
|
||||||
|
"losSelectStartEnd": "Wybierz węzły początkowe i końcowe dla LOS.",
|
||||||
|
"losRunFailed": "Sprawdzenie pola widzenia nie powiodło się: {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Wyczyść wszystkie punkty",
|
||||||
|
"losRunToViewElevationProfile": "Uruchom LOS, aby wyświetlić profil wysokości",
|
||||||
|
"losMenuTitle": "Menu LOS",
|
||||||
|
"losMenuSubtitle": "Stuknij węzły lub naciśnij i przytrzymaj mapę, aby uzyskać niestandardowe punkty",
|
||||||
|
"losShowDisplayNodes": "Pokaż węzły wyświetlające",
|
||||||
|
"losCustomPoints": "Punkty niestandardowe",
|
||||||
|
"losCustomPointLabel": "Niestandardowe {index}",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Punkt A",
|
||||||
|
"losPointB": "Punkt B",
|
||||||
|
"losAntennaA": "Antena A: {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Antena B: {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Uruchom LOS-a",
|
||||||
|
"losNoElevationData": "Brak danych o wysokości",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, czysty LOS, minimalny prześwit {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, zablokowane przez {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "LOS: sprawdzam...",
|
||||||
|
"losStatusNoData": "LOS: brak danych",
|
||||||
|
"losStatusSummary": "LOS: {clear}/{total} jasne, {blocked} zablokowane, {unknown} nieznane",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Dane dotyczące wysokości są niedostępne dla jednej lub większej liczby próbek.",
|
||||||
|
"losErrorInvalidInput": "Nieprawidłowe dane punktów/wysokości do obliczenia LOS.",
|
||||||
|
"losRenameCustomPoint": "Zmień nazwę punktu niestandardowego",
|
||||||
|
"losPointName": "Nazwa punktu",
|
||||||
|
"losShowPanelTooltip": "Pokaż panel LOS",
|
||||||
|
"losHidePanelTooltip": "Ukryj panel LOS",
|
||||||
|
"losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Horyzont radiowy",
|
||||||
|
"losLegendLosBeam": "Linia widoczności",
|
||||||
|
"losLegendTerrain": "Teren",
|
||||||
|
"losFrequencyLabel": "Częstotliwość",
|
||||||
|
"losFrequencyInfoTooltip": "Zobacz szczegóły obliczenia",
|
||||||
|
"losFrequencyDialogTitle": "Obliczanie horyzontu radiowego",
|
||||||
|
"losFrequencyDialogDescription": "Zaczynając od k={baselineK} przy {baselineFreq} MHz, obliczenia korygują współczynnik k dla bieżącego pasma {frequencyMHz} MHz, które definiuje zakrzywiony limit horyzontu radiowego.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listFilter_removeFromFavorites": "Usuń z ulubionych",
|
||||||
|
"listFilter_addToFavorites": "Dodaj do ulubionych",
|
||||||
|
"listFilter_favorites": "Ulubione",
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_unread": "Nieprzeczytane",
|
||||||
|
"contacts_searchContactsNoNumber": "Wyszukaj kontakty...",
|
||||||
|
"contacts_searchFavorites": "Wyszukaj {number}{str} ulubione...",
|
||||||
|
"contacts_searchRoomServers": "Wyszukaj {number}{str} serwerów Room...",
|
||||||
|
"contacts_searchUsers": "Wyszukaj {number}{str} Użytkowników...",
|
||||||
|
"contacts_searchRepeaters": "Wyszukaj {number}{str} powtórników...",
|
||||||
|
"contactsSettings_title": "Ustawienia kontaktów",
|
||||||
|
"settings_contactSettingsSubtitle": "Ustawienia dotyczące sposobu dodawania kontaktów",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Pozwól towarzyszowi automatycznie dodawać znalezione użytkowników.",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Automatyczne dodawanie powtarzalników",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Zezwól na automatyczne dodawanie odkrytych repeaterów przez towarzysza.",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Automatycznie dodaj serwery pokojowe",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Automatycznie dodaj użytkowników",
|
||||||
|
"settings_contactSettings": "Ustawienia kontaktowe",
|
||||||
|
"contactsSettings_otherTitle": "Inne ustawienia związane z kontaktami",
|
||||||
|
"contactsSettings_autoAddTitle": "Automatyczne odnajdywanie",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie znalezionych serwerów pokojowych.",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Automatycznie dodaj czujniki",
|
||||||
|
"discoveredContacts_searchHint": "Wyszukaj odkryte kontakty",
|
||||||
|
"discoveredContacts_contactAdded": "Kontakt dodany",
|
||||||
|
"discoveredContacts_addContact": "Dodaj kontakt",
|
||||||
|
"discoveredContacts_copyContact": "Kopiuj kontakt do schowka",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Nadpisz najstarszy",
|
||||||
|
"discoveredContacts_Title": "Odkryte Kontakty",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie wykrytych czujników.",
|
||||||
|
"discoveredContacts_noMatching": "Brak pasujących kontaktów",
|
||||||
|
"discoveredContacts_deleteContact": "Usuń kontakt",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Gdy lista kontaktów jest pełna, najstarszy nieulubiony kontakt zostanie zastąpiony.",
|
||||||
|
"common_deleteAll": "Usuń wszystko",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Czy na pewno chcesz usunąć wszystkie znalezione kontakty?",
|
||||||
|
"discoveredContacts_deleteContactAll": "Usuń wszystkie odkryte kontakty",
|
||||||
|
"map_guessedLocation": "Wydana lokalizacja",
|
||||||
|
"map_showGuessedLocations": "Wyświetl lokalizacje zgadanych węzłów",
|
||||||
|
"usbScreenSubtitle": "Wybierz wykryty urządzenie szeregowe i podłącz je bezpośrednio do swojego węzła MeshCore.",
|
||||||
|
"usbScreenTitle": "Połącz przez USB",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenStatus": "Wybierz urządzenie USB",
|
||||||
|
"usbScreenNote": "Port szeregowy USB jest aktywny na urządzeniach z systemem Android i platformach stacjonarnych, które go obsługują.",
|
||||||
|
"usbScreenEmptyState": "Nie znaleziono żadnych urządzeń USB. Podłącz jedno i zaktualizuj.",
|
||||||
|
"usbErrorPermissionDenied": "Zostało odrzucone żądanie dostępu przez USB.",
|
||||||
|
"usbErrorDeviceMissing": "Wybór urządzenia USB już nie jest dostępny.",
|
||||||
|
"usbErrorInvalidPort": "Wybierz prawidłowe urządzenie USB.",
|
||||||
|
"usbErrorBusy": "Kolejne żądanie połączenia przez USB jest już w trakcie realizacji.",
|
||||||
|
"usbErrorNotConnected": "Brak podłączonego urządzenia USB.",
|
||||||
|
"usbErrorOpenFailed": "Nie udało się otworzyć wybranego urządzenia USB.",
|
||||||
|
"usbErrorConnectFailed": "Nie udało się połączyć z wybranym urządzeniem USB.",
|
||||||
|
"usbErrorUnsupported": "Port szeregowy USB nie jest obsługiwany na tym urządzeniu.",
|
||||||
|
"usbErrorAlreadyActive": "Połączenie USB jest już aktywne.",
|
||||||
|
"usbErrorNoDeviceSelected": "Nie został wybrany żaden urządzenie USB.",
|
||||||
|
"usbErrorPortClosed": "Połączenie USB nie jest aktywne.",
|
||||||
|
"usbFallbackDeviceName": "Urządzenie do komunikacji przez sieć (seria)",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_searching": "Wyszukiwanie urządzeń USB...",
|
||||||
|
"usbStatus_connecting": "Połączenie z urządzeniem USB...",
|
||||||
|
"usbStatus_notConnected": "Wybierz urządzenie USB",
|
||||||
|
"usbConnectionFailed": "Błąd połączenia USB: {error}",
|
||||||
|
"usbErrorConnectTimedOut": "Połączenie nie zostało nawiązane. Upewnij się, że urządzenie posiada oprogramowanie \"USB Companion\"."
|
||||||
}
|
}
|
||||||
|
|||||||
+267
-4
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"channels_channelDeleteFailed": "Falha ao excluir o canal \"{name}\"",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@@locale": "pt",
|
"@@locale": "pt",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Contactos",
|
"nav_contacts": "Contactos",
|
||||||
@@ -334,6 +342,8 @@
|
|||||||
"channels_publicChannel": "Canal público",
|
"channels_publicChannel": "Canal público",
|
||||||
"channels_privateChannel": "Canal privado",
|
"channels_privateChannel": "Canal privado",
|
||||||
"channels_editChannel": "Editar canal",
|
"channels_editChannel": "Editar canal",
|
||||||
|
"channels_muteChannel": "Silenciar canal",
|
||||||
|
"channels_unmuteChannel": "Ativar canal",
|
||||||
"channels_deleteChannel": "Excluir canal",
|
"channels_deleteChannel": "Excluir canal",
|
||||||
"channels_deleteChannelConfirm": "Excluir \"{name}\"? Não pode ser desfeito.",
|
"channels_deleteChannelConfirm": "Excluir \"{name}\"? Não pode ser desfeito.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
@@ -1351,12 +1361,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Vizinhos",
|
"repeater_neighbors": "Vizinhos",
|
||||||
"neighbors_receivedData": "Dados dos Vizinhos Recebidos",
|
"neighbors_receivedData": "Dados dos Vizinhos Recebidos",
|
||||||
"repeater_neighboursSubtitle": "Visualizar vizinhos de salto zero.",
|
"repeater_neighborsSubtitle": "Visualizar vizinhos de salto zero.",
|
||||||
"neighbors_requestTimedOut": "Vizinhos solicitam tempo limite esgotado.",
|
"neighbors_requestTimedOut": "Vizinhos solicitam tempo limite esgotado.",
|
||||||
"neighbors_errorLoading": "Erro ao carregar vizinhos: {error}",
|
"neighbors_errorLoading": "Erro ao carregar vizinhos: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Repetidores Vizinhos",
|
"neighbors_repeatersNeighbors": "Repetidores Vizinhos",
|
||||||
"neighbors_noData": "Não estão disponíveis dados de vizinhos.",
|
"neighbors_noData": "Não estão disponíveis dados de vizinhos.",
|
||||||
"channels_createPrivateChannelDesc": "Protegido com uma chave secreta.",
|
"channels_createPrivateChannelDesc": "Protegido com uma chave secreta.",
|
||||||
"channels_joinPrivateChannelDesc": "Inserir uma chave secreta manualmente.",
|
"channels_joinPrivateChannelDesc": "Inserir uma chave secreta manualmente.",
|
||||||
@@ -1556,6 +1566,8 @@
|
|||||||
"contacts_copyAdvertToClipboard": "Copiar Anúncio para Área de Transferência",
|
"contacts_copyAdvertToClipboard": "Copiar Anúncio para Área de Transferência",
|
||||||
"contacts_addContactFromClipboard": "Adicionar Contato da Área de Transferência",
|
"contacts_addContactFromClipboard": "Adicionar Contato da Área de Transferência",
|
||||||
"appSettings_languageRu": "Russo",
|
"appSettings_languageRu": "Russo",
|
||||||
|
"appSettings_enableMessageTracing": "Ativar rastreamento de mensagens",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Mostrar metadados detalhados de roteamento e tempo para as mensagens",
|
||||||
"contacts_ShareContact": "Copiar contato para Área de Transferência",
|
"contacts_ShareContact": "Copiar contato para Área de Transferência",
|
||||||
"contacts_contactImportFailed": "Contato falhou ao ser importado.",
|
"contacts_contactImportFailed": "Contato falhou ao ser importado.",
|
||||||
"contacts_zeroHopContactAdvertSent": "Enviou contato por anúncio.",
|
"contacts_zeroHopContactAdvertSent": "Enviou contato por anúncio.",
|
||||||
@@ -1594,7 +1606,258 @@
|
|||||||
"scanner_enableBluetooth": "Ative o Bluetooth",
|
"scanner_enableBluetooth": "Ative o Bluetooth",
|
||||||
"scanner_bluetoothOff": "Bluetooth está desativado",
|
"scanner_bluetoothOff": "Bluetooth está desativado",
|
||||||
"scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos.",
|
"scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos.",
|
||||||
|
"scanner_chromeRequired": "Navegador Chrome necessário",
|
||||||
|
"scanner_chromeRequiredMessage": "Esta aplicação web requer o Google Chrome ou um navegador baseado no Chromium para suporte de Bluetooth.",
|
||||||
|
"snrIndicator_nearByRepeaters": "Repetidores Próximos",
|
||||||
|
"snrIndicator_lastSeen": "Visto pela última vez",
|
||||||
|
"chat_ShowAllPaths": "Mostrar todos os caminhos",
|
||||||
"settings_clientRepeatFreqWarning": "A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.",
|
"settings_clientRepeatFreqWarning": "A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.",
|
||||||
"settings_clientRepeat": "Repetição sem rede",
|
"settings_clientRepeat": "Repetição sem rede",
|
||||||
"settings_clientRepeatSubtitle": "Permita que este dispositivo repita pacotes de rede para outros dispositivos."
|
"settings_clientRepeatSubtitle": "Permita que este dispositivo repita pacotes de rede para outros dispositivos.",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "Dados de elevação LOS: Open-Meteo (CC BY 4.0)",
|
||||||
|
"appSettings_unitsTitle": "Unidades",
|
||||||
|
"appSettings_unitsMetric": "Métrico (m/km)",
|
||||||
|
"appSettings_unitsImperial": "Imperial (ft/mi)",
|
||||||
|
"map_lineOfSight": "Linha de visão",
|
||||||
|
"map_losScreenTitle": "Linha de visão",
|
||||||
|
"losSelectStartEnd": "Selecione nós iniciais e finais para LOS.",
|
||||||
|
"losRunFailed": "Falha na verificação da linha de visão: {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Limpe todos os pontos",
|
||||||
|
"losRunToViewElevationProfile": "Execute o LOS para visualizar o perfil de elevação",
|
||||||
|
"losMenuTitle": "Menu LOS",
|
||||||
|
"losMenuSubtitle": "Toque nos nós ou mantenha pressionado o mapa para obter pontos personalizados",
|
||||||
|
"losShowDisplayNodes": "Mostrar nós de exibição",
|
||||||
|
"losCustomPoints": "Pontos personalizados",
|
||||||
|
"losCustomPointLabel": "{index} personalizado",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Ponto A",
|
||||||
|
"losPointB": "Ponto B",
|
||||||
|
"losAntennaA": "Antena A: {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Antena B: {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Executar LOS",
|
||||||
|
"losNoElevationData": "Sem dados de elevação",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, limpar LOS, liberação mínima {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, bloqueado por {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "LOS: verificando...",
|
||||||
|
"losStatusNoData": "LOS: sem dados",
|
||||||
|
"losStatusSummary": "LOS: {clear}/{total} limpo, {blocked} bloqueado, {unknown} desconhecido",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Dados de elevação indisponíveis para uma ou mais amostras.",
|
||||||
|
"losErrorInvalidInput": "Dados de pontos/elevação inválidos para cálculo de LOS.",
|
||||||
|
"losRenameCustomPoint": "Renomear ponto personalizado",
|
||||||
|
"losPointName": "Nome do ponto",
|
||||||
|
"losShowPanelTooltip": "Mostrar painel LOS",
|
||||||
|
"losHidePanelTooltip": "Ocultar painel LOS",
|
||||||
|
"losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Horizonte de rádio",
|
||||||
|
"losLegendLosBeam": "Linha de visada",
|
||||||
|
"losLegendTerrain": "Terreno",
|
||||||
|
"losFrequencyLabel": "Frequência",
|
||||||
|
"losFrequencyInfoTooltip": "Ver detalhes do cálculo",
|
||||||
|
"losFrequencyDialogTitle": "Cálculo do horizonte de rádio",
|
||||||
|
"losFrequencyDialogDescription": "Começando em k={baselineK} em {baselineFreq} MHz, o cálculo ajusta o fator k para a banda atual de {frequencyMHz} MHz, que define o limite do horizonte de rádio curvo.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listFilter_addToFavorites": "Adicionar aos favoritos",
|
||||||
|
"listFilter_removeFromFavorites": "Remover da lista de favoritos",
|
||||||
|
"listFilter_favorites": "Favoritos",
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_searchRepeaters": "Pesquisar {number}{str} Repetidores...",
|
||||||
|
"contacts_searchFavorites": "Pesquisar {number}{str} Favoritos...",
|
||||||
|
"contacts_searchUsers": "Pesquisar {number}{str} Usuários...",
|
||||||
|
"contacts_searchContactsNoNumber": "Pesquisar Contatos...",
|
||||||
|
"contacts_unread": "Não lido",
|
||||||
|
"contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala...",
|
||||||
|
"settings_contactSettings": "Configurações de Contato",
|
||||||
|
"contactsSettings_otherTitle": "Outras configurações relacionadas a contatos",
|
||||||
|
"contactsSettings_title": "Configurações de contatos",
|
||||||
|
"contactsSettings_autoAddTitle": "Descoberta Automática",
|
||||||
|
"settings_contactSettingsSubtitle": "Configurações para como os contatos são adicionados",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Adicionar usuários automaticamente",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Permitir que o companheiro adicione automaticamente os repetidores descobertos.",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Adicionar automaticamente servidores de sala",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Sobrescrever o Mais Antigo",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Adicionar sensores automaticamente",
|
||||||
|
"discoveredContacts_Title": "Contatos Descobertos",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Permitir que o companheiro adicione automaticamente os usuários descobertos.",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Adicionar repetidores automaticamente",
|
||||||
|
"discoveredContacts_noMatching": "Nenhum contato correspondente",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Permitir que o companheiro adicione automaticamente os servidores de salas descobertos.",
|
||||||
|
"discoveredContacts_searchHint": "Pesquisar contatos descobertos",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Permitir que o companheiro adicione automaticamente sensores descobertos.",
|
||||||
|
"discoveredContacts_copyContact": "Copiar Contato para a área de transferência",
|
||||||
|
"discoveredContacts_deleteContact": "Excluir Contato",
|
||||||
|
"discoveredContacts_contactAdded": "Contato adicionado",
|
||||||
|
"discoveredContacts_addContact": "Adicionar Contato",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Quando a lista de contatos estiver cheia, o contato mais antigo não favoritado será substituído.",
|
||||||
|
"common_deleteAll": "Excluir Tudo",
|
||||||
|
"discoveredContacts_deleteContactAll": "Excluir Todos os Contatos Descobertos",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Tem certeza de que deseja excluir todos os contatos descobertos?",
|
||||||
|
"map_guessedLocation": "Localização estimada",
|
||||||
|
"map_showGuessedLocations": "Mostrar as localizações dos nós estimados",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenTitle": "Conecte via USB",
|
||||||
|
"usbScreenSubtitle": "Selecione um dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.",
|
||||||
|
"usbScreenStatus": "Selecione um dispositivo USB",
|
||||||
|
"usbScreenNote": "A comunicação serial via USB está ativa em dispositivos Android e plataformas de desktop compatíveis.",
|
||||||
|
"usbScreenEmptyState": "Nenhum dispositivo USB encontrado. Conecte um e atualize.",
|
||||||
|
"usbErrorPermissionDenied": "A permissão para acesso via USB foi negada.",
|
||||||
|
"usbErrorDeviceMissing": "O dispositivo USB selecionado não está mais disponível.",
|
||||||
|
"usbErrorInvalidPort": "Selecione um dispositivo USB válido.",
|
||||||
|
"usbErrorBusy": "Já existe uma solicitação de conexão USB em andamento.",
|
||||||
|
"usbErrorNotConnected": "Não há nenhum dispositivo USB conectado.",
|
||||||
|
"usbErrorOpenFailed": "Não foi possível abrir o dispositivo USB selecionado.",
|
||||||
|
"usbErrorConnectFailed": "Não foi possível conectar ao dispositivo USB selecionado.",
|
||||||
|
"usbErrorUnsupported": "A comunicação serial via USB não é suportada nesta plataforma.",
|
||||||
|
"usbErrorAlreadyActive": "A conexão USB já está ativa.",
|
||||||
|
"usbErrorNoDeviceSelected": "Nenhum dispositivo USB foi selecionado.",
|
||||||
|
"usbErrorPortClosed": "A conexão USB não está ativa.",
|
||||||
|
"usbFallbackDeviceName": "Dispositivo de Serial para a Web",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_searching": "Procurando por dispositivos USB...",
|
||||||
|
"usbStatus_notConnected": "Selecione um dispositivo USB",
|
||||||
|
"usbConnectionFailed": "Falha na conexão USB: {error}",
|
||||||
|
"usbStatus_connecting": "Conectando ao dispositivo USB...",
|
||||||
|
"usbErrorConnectTimedOut": "A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion."
|
||||||
}
|
}
|
||||||
|
|||||||
+267
-4
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"channels_channelDeleteFailed": "Не удалось удалить канал {name}.",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@@locale": "ru",
|
"@@locale": "ru",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Контакты",
|
"nav_contacts": "Контакты",
|
||||||
@@ -226,6 +234,8 @@
|
|||||||
"channels_publicChannel": "Публичный канал",
|
"channels_publicChannel": "Публичный канал",
|
||||||
"channels_privateChannel": "Приватный канал",
|
"channels_privateChannel": "Приватный канал",
|
||||||
"channels_editChannel": "Изменить канал",
|
"channels_editChannel": "Изменить канал",
|
||||||
|
"channels_muteChannel": "Отключить уведомления канала",
|
||||||
|
"channels_unmuteChannel": "Включить уведомления канала",
|
||||||
"channels_deleteChannel": "Удалить канал",
|
"channels_deleteChannel": "Удалить канал",
|
||||||
"channels_deleteChannelConfirm": "Удалить \"{name}\"? Это действие нельзя отменить.",
|
"channels_deleteChannelConfirm": "Удалить \"{name}\"? Это действие нельзя отменить.",
|
||||||
"channels_channelDeleted": "Канал \"{name}\" удалён",
|
"channels_channelDeleted": "Канал \"{name}\" удалён",
|
||||||
@@ -467,8 +477,8 @@
|
|||||||
"repeater_telemetrySubtitle": "Просмотр телеметрии датчиков и системной статистики",
|
"repeater_telemetrySubtitle": "Просмотр телеметрии датчиков и системной статистики",
|
||||||
"repeater_cli": "CLI",
|
"repeater_cli": "CLI",
|
||||||
"repeater_cliSubtitle": "Отправка команд репитеру",
|
"repeater_cliSubtitle": "Отправка команд репитеру",
|
||||||
"repeater_neighbours": "Соседи",
|
"repeater_neighbors": "Соседи",
|
||||||
"repeater_neighboursSubtitle": "Просмотр соседей на нулевом хопе.",
|
"repeater_neighborsSubtitle": "Просмотр соседей на нулевом хопе.",
|
||||||
"repeater_settings": "Настройки",
|
"repeater_settings": "Настройки",
|
||||||
"repeater_settingsSubtitle": "Настройка параметров репитера",
|
"repeater_settingsSubtitle": "Настройка параметров репитера",
|
||||||
"repeater_statusTitle": "Статус репитера",
|
"repeater_statusTitle": "Статус репитера",
|
||||||
@@ -661,7 +671,7 @@
|
|||||||
"neighbors_receivedData": "Полученные данные о соседях",
|
"neighbors_receivedData": "Полученные данные о соседях",
|
||||||
"neighbors_requestTimedOut": "Время ожидания данных о соседях истекло.",
|
"neighbors_requestTimedOut": "Время ожидания данных о соседях истекло.",
|
||||||
"neighbors_errorLoading": "Ошибка загрузки соседей: {error}",
|
"neighbors_errorLoading": "Ошибка загрузки соседей: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Соседи репитеров",
|
"neighbors_repeatersNeighbors": "Соседи репитеров",
|
||||||
"neighbors_noData": "Данные о соседях недоступны.",
|
"neighbors_noData": "Данные о соседях недоступны.",
|
||||||
"neighbors_unknownContact": "Неизвестный {pubkey}",
|
"neighbors_unknownContact": "Неизвестный {pubkey}",
|
||||||
"neighbors_heardA ago": "Слышали: {time} назад",
|
"neighbors_heardA ago": "Слышали: {time} назад",
|
||||||
@@ -794,6 +804,8 @@
|
|||||||
"contacts_invalidAdvertFormat": "Недействительные контактные данные",
|
"contacts_invalidAdvertFormat": "Недействительные контактные данные",
|
||||||
"contacts_zeroHopAdvert": "Реклама Zero Hop",
|
"contacts_zeroHopAdvert": "Реклама Zero Hop",
|
||||||
"appSettings_languageUk": "Українська",
|
"appSettings_languageUk": "Українська",
|
||||||
|
"appSettings_enableMessageTracing": "Включить трассировку сообщений",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Показывать подробные метаданные о маршрутизации и времени для сообщений",
|
||||||
"contacts_floodAdvert": "Рекламный поток",
|
"contacts_floodAdvert": "Рекламный поток",
|
||||||
"contacts_clipboardEmpty": "Буфер обмена пуст.",
|
"contacts_clipboardEmpty": "Буфер обмена пуст.",
|
||||||
"contacts_copyAdvertToClipboard": "Копировать рекламу в буфер обмена",
|
"contacts_copyAdvertToClipboard": "Копировать рекламу в буфер обмена",
|
||||||
@@ -834,7 +846,258 @@
|
|||||||
"scanner_enableBluetooth": "Включите Bluetooth",
|
"scanner_enableBluetooth": "Включите Bluetooth",
|
||||||
"scanner_bluetoothOff": "Bluetooth выключен",
|
"scanner_bluetoothOff": "Bluetooth выключен",
|
||||||
"scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства.",
|
"scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства.",
|
||||||
|
"scanner_chromeRequired": "Требуется браузер Chrome",
|
||||||
|
"scanner_chromeRequiredMessage": "Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.",
|
||||||
|
"snrIndicator_nearByRepeaters": "Ближайшие ретрансляторы",
|
||||||
|
"snrIndicator_lastSeen": "Последний раз видели",
|
||||||
|
"chat_ShowAllPaths": "Показать все пути",
|
||||||
"settings_clientRepeatFreqWarning": "Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.",
|
"settings_clientRepeatFreqWarning": "Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.",
|
||||||
"settings_clientRepeatSubtitle": "Позвольте этому устройству повторять пакеты данных для других устройств.",
|
"settings_clientRepeatSubtitle": "Позвольте этому устройству повторять пакеты данных для других устройств.",
|
||||||
"settings_clientRepeat": "Повторение \"вне сети\""
|
"settings_clientRepeat": "Повторение \"вне сети\"",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "Данные о высоте LOS: Open-Meteo (CC BY 4.0)",
|
||||||
|
"appSettings_unitsTitle": "Единицы",
|
||||||
|
"appSettings_unitsMetric": "Метрическая (м/км)",
|
||||||
|
"appSettings_unitsImperial": "Имперская (ft / mi)",
|
||||||
|
"map_lineOfSight": "Линия видимости",
|
||||||
|
"map_losScreenTitle": "Линия видимости",
|
||||||
|
"losSelectStartEnd": "Выберите начальный и конечный узлы для LOS.",
|
||||||
|
"losRunFailed": "Проверка прямой видимости не удалась: {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Очистить все точки",
|
||||||
|
"losRunToViewElevationProfile": "Запустите LOS, чтобы просмотреть профиль высот.",
|
||||||
|
"losMenuTitle": "ЛОС Меню",
|
||||||
|
"losMenuSubtitle": "Коснитесь узлов или нажмите и удерживайте карту для выбора пользовательских точек.",
|
||||||
|
"losShowDisplayNodes": "Показать узлы отображения",
|
||||||
|
"losCustomPoints": "Пользовательские точки",
|
||||||
|
"losCustomPointLabel": "Пользовательский {index}",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Точка А",
|
||||||
|
"losPointB": "Точка Б",
|
||||||
|
"losAntennaA": "Антенна А: {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Антенна Б: {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Запустить ЛОС",
|
||||||
|
"losNoElevationData": "Нет данных о высоте",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, свободная зона видимости, минимальный зазор {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, заблокирован {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "ЛОС: проверяю...",
|
||||||
|
"losStatusNoData": "ЛОС: нет данных",
|
||||||
|
"losStatusSummary": "LOS: {clear}/{total} очищено, {blocked} заблокировано, {unknown} неизвестно.",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Данные о высоте недоступны для одного или нескольких образцов.",
|
||||||
|
"losErrorInvalidInput": "Неверные данные о точках/высоте для расчета LOS.",
|
||||||
|
"losRenameCustomPoint": "Переименовать пользовательскую точку",
|
||||||
|
"losPointName": "Имя точки",
|
||||||
|
"losShowPanelTooltip": "Показать панель LOS",
|
||||||
|
"losHidePanelTooltip": "Скрыть панель LOS",
|
||||||
|
"losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Радиогоризонт",
|
||||||
|
"losLegendLosBeam": "Линия прямой видимости",
|
||||||
|
"losLegendTerrain": "Рельеф",
|
||||||
|
"losFrequencyLabel": "Частота",
|
||||||
|
"losFrequencyInfoTooltip": "Просмотреть детали расчёта",
|
||||||
|
"losFrequencyDialogTitle": "Расчёт радиогоризонта",
|
||||||
|
"losFrequencyDialogDescription": "Начиная с k={baselineK} на частоте {baselineFreq} МГц, расчет корректирует коэффициент k для текущего диапазона {frequencyMHz} МГц, который определяет изогнутую границу радиогоризонта.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listFilter_addToFavorites": "Добавить в избранное",
|
||||||
|
"listFilter_favorites": "Избранное",
|
||||||
|
"listFilter_removeFromFavorites": "Удалить из избранного",
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_searchRepeaters": "Поиск {number}{str} ретрансляторов...",
|
||||||
|
"contacts_searchContactsNoNumber": "Поиск контактов...",
|
||||||
|
"contacts_unread": "Непрочитанное",
|
||||||
|
"contacts_searchRoomServers": "Поиск {number}{str} серверов комнат...",
|
||||||
|
"contacts_searchFavorites": "Поиск {number}{str} избранного...",
|
||||||
|
"contacts_searchUsers": "Поиск {number}{str} пользователей...",
|
||||||
|
"settings_contactSettings": "Настройки контактов",
|
||||||
|
"settings_contactSettingsSubtitle": "Настройки добавления контактов",
|
||||||
|
"contactsSettings_autoAddTitle": "Автоматическое обнаружение",
|
||||||
|
"contactsSettings_title": "Настройки контактов",
|
||||||
|
"contactsSettings_otherTitle": "Другие настройки, связанные с контактами",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Разрешить компаньону автоматически добавлять обнаруженных пользователей",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Автоматически добавлять серверы комнат",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Автоматически добавлять датчики",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Разрешить компаньону автоматически добавлять обнаруженные датчики",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Автоматически добавлять пользователей",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Перезаписать самое старое",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Автоматически добавлять ретрансляторы",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Разрешить спутнику автоматически добавлять обнаруженные ретрансляторы",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Разрешить компаньону автоматически добавлять обнаруженные сервера комнат.",
|
||||||
|
"discoveredContacts_noMatching": "Нет совпадающих контактов",
|
||||||
|
"discoveredContacts_searchHint": "Найденные контакты поиска",
|
||||||
|
"discoveredContacts_contactAdded": "Контакт добавлен",
|
||||||
|
"discoveredContacts_copyContact": "Копировать контакт в буфер обмена",
|
||||||
|
"discoveredContacts_addContact": "Добавить контакт",
|
||||||
|
"discoveredContacts_Title": "Обнаруженные контакты",
|
||||||
|
"discoveredContacts_deleteContact": "Удалить контакт",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Когда список контактов заполнен, будет заменен самый старый контакт, который не находится в избранном.",
|
||||||
|
"common_deleteAll": "Удалить все",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Вы уверены, что хотите удалить все обнаруженные контакты?",
|
||||||
|
"discoveredContacts_deleteContactAll": "Удалить Все Обнаруженные Контакты",
|
||||||
|
"map_guessedLocation": "Угаданное место",
|
||||||
|
"map_showGuessedLocations": "Отобразить предполагаемые места расположения узлов",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenSubtitle": "Выберите обнаруженное устройство с последовательным интерфейсом и подключите его напрямую к вашему узлу MeshCore.",
|
||||||
|
"usbScreenTitle": "Подключение через USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenStatus": "Выберите USB-устройство",
|
||||||
|
"usbScreenNote": "USB-серийный порт активен на поддерживаемых устройствах Android и на настольных платформах.",
|
||||||
|
"usbScreenEmptyState": "Не обнаружено устройств USB. Подключите одно из них и обновите список.",
|
||||||
|
"usbErrorPermissionDenied": "Запрос на доступ через USB был отклонен.",
|
||||||
|
"usbErrorDeviceMissing": "Выбранное USB-устройство больше недоступно.",
|
||||||
|
"usbErrorInvalidPort": "Выберите действительное USB-устройство.",
|
||||||
|
"usbErrorBusy": "Еще одно запрошенное соединение через USB уже находится в процессе.",
|
||||||
|
"usbErrorNotConnected": "Ни одно USB-устройство не подключено.",
|
||||||
|
"usbErrorOpenFailed": "Не удалось открыть выбранное USB-устройство.",
|
||||||
|
"usbErrorConnectFailed": "Не удалось установить соединение с выбранным USB-устройством.",
|
||||||
|
"usbErrorUnsupported": "Поддержка последовательного USB отсутствует на данной платформе.",
|
||||||
|
"usbErrorAlreadyActive": "USB-соединение уже установлено.",
|
||||||
|
"usbErrorNoDeviceSelected": "Не было выбрано ни одно устройство USB.",
|
||||||
|
"usbErrorPortClosed": "USB-соединение не установлено.",
|
||||||
|
"usbFallbackDeviceName": "Устройство для последовательного подключения к сети",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_searching": "Поиск USB-устройств...",
|
||||||
|
"usbStatus_connecting": "Подключение к USB-устройству...",
|
||||||
|
"usbConnectionFailed": "Не удалось установить соединение через USB: {error}",
|
||||||
|
"usbStatus_notConnected": "Выберите USB-устройство",
|
||||||
|
"usbErrorConnectTimedOut": "Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion."
|
||||||
}
|
}
|
||||||
|
|||||||
+267
-4
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"channels_channelDeleteFailed": "Kanál \"{name}\" sa nepodarilo odstrániť",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@@locale": "sk",
|
"@@locale": "sk",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Kontakty",
|
"nav_contacts": "Kontakty",
|
||||||
@@ -334,6 +342,8 @@
|
|||||||
"channels_publicChannel": "Veľké verejne kanály",
|
"channels_publicChannel": "Veľké verejne kanály",
|
||||||
"channels_privateChannel": "Osobné kanál",
|
"channels_privateChannel": "Osobné kanál",
|
||||||
"channels_editChannel": "Upraviť kanál",
|
"channels_editChannel": "Upraviť kanál",
|
||||||
|
"channels_muteChannel": "Stlmiť kanál",
|
||||||
|
"channels_unmuteChannel": "Zrušiť stlmenie kanála",
|
||||||
"channels_deleteChannel": "Odstrániť kanál",
|
"channels_deleteChannel": "Odstrániť kanál",
|
||||||
"channels_deleteChannelConfirm": "Odstrániť \"{name}\"? To sa nedá zrušiť.",
|
"channels_deleteChannelConfirm": "Odstrániť \"{name}\"? To sa nedá zrušiť.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
@@ -1351,12 +1361,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighboursSubtitle": "Zobraziť susedné body bez skokov.",
|
"repeater_neighborsSubtitle": "Zobraziť susedné body bez skokov.",
|
||||||
"neighbors_requestTimedOut": "Súďia žiadajú o časové ukončenie.",
|
"neighbors_requestTimedOut": "Súďia žiadajú o časové ukončenie.",
|
||||||
"neighbors_receivedData": "Obdielo dáta suseda",
|
"neighbors_receivedData": "Obdielo dáta suseda",
|
||||||
"repeater_neighbours": "Súsezný",
|
"repeater_neighbors": "Súsezný",
|
||||||
"neighbors_errorLoading": "Chyba pri načítaní susedov: {error}",
|
"neighbors_errorLoading": "Chyba pri načítaní susedov: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Opakovadlá Súsezná",
|
"neighbors_repeatersNeighbors": "Opakovadlá Súsezná",
|
||||||
"neighbors_noData": "Nie je dostupná žiadna informácia o susedoch.",
|
"neighbors_noData": "Nie je dostupná žiadna informácia o susedoch.",
|
||||||
"channels_createPrivateChannel": "Vytvorte súkromný kanál",
|
"channels_createPrivateChannel": "Vytvorte súkromný kanál",
|
||||||
"channels_joinPrivateChannel": "Pripojiť sa k súkromnému kanálu",
|
"channels_joinPrivateChannel": "Pripojiť sa k súkromnému kanálu",
|
||||||
@@ -1556,6 +1566,8 @@
|
|||||||
"contacts_copyAdvertToClipboard": "Kopírovať reklamu do schránky",
|
"contacts_copyAdvertToClipboard": "Kopírovať reklamu do schránky",
|
||||||
"contacts_invalidAdvertFormat": "Neplatné kontaktné údaje",
|
"contacts_invalidAdvertFormat": "Neplatné kontaktné údaje",
|
||||||
"appSettings_languageRu": "Ruština",
|
"appSettings_languageRu": "Ruština",
|
||||||
|
"appSettings_enableMessageTracing": "Povoliť sledovanie správ",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Zobraziť podrobné metadáta o smerovaní a časovaní správ",
|
||||||
"contacts_addContactFromClipboard": "Pridať kontakt z schránky",
|
"contacts_addContactFromClipboard": "Pridať kontakt z schránky",
|
||||||
"contacts_contactImported": "Kontakt bol importovaný.",
|
"contacts_contactImported": "Kontakt bol importovaný.",
|
||||||
"contacts_zeroHopContactAdvertSent": "Poslal kontakt cez inzerát.",
|
"contacts_zeroHopContactAdvertSent": "Poslal kontakt cez inzerát.",
|
||||||
@@ -1592,9 +1604,260 @@
|
|||||||
"map_runTrace": "Spustiť trasovaním cesty",
|
"map_runTrace": "Spustiť trasovaním cesty",
|
||||||
"map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené.",
|
"map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené.",
|
||||||
"scanner_bluetoothOffMessage": "Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.",
|
"scanner_bluetoothOffMessage": "Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.",
|
||||||
|
"scanner_chromeRequired": "Vyžaduje sa prehliadač Chrome",
|
||||||
|
"scanner_chromeRequiredMessage": "Táto webová aplikácia vyžaduje Google Chrome alebo prehliadač založený na Chromium pre podporu Bluetooth.",
|
||||||
"scanner_bluetoothOff": "Bluetooth je vypnutý",
|
"scanner_bluetoothOff": "Bluetooth je vypnutý",
|
||||||
"scanner_enableBluetooth": "Povolte Bluetooth",
|
"scanner_enableBluetooth": "Povolte Bluetooth",
|
||||||
|
"snrIndicator_lastSeen": "Naposledy videný",
|
||||||
|
"snrIndicator_nearByRepeaters": "Miestne opakovače",
|
||||||
|
"chat_ShowAllPaths": "Zobraziť všetky cesty",
|
||||||
"settings_clientRepeat": "Opätovné použitie bez elektrickej siete",
|
"settings_clientRepeat": "Opätovné použitie bez elektrickej siete",
|
||||||
"settings_clientRepeatFreqWarning": "Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.",
|
"settings_clientRepeatFreqWarning": "Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.",
|
||||||
"settings_clientRepeatSubtitle": "Umožnite, aby toto zariadenie opakovávalo siete pre ostatných."
|
"settings_clientRepeatSubtitle": "Umožnite, aby toto zariadenie opakovávalo siete pre ostatných.",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "Údaje o nadmorskej výške LOS: Open-Meteo (CC BY 4.0)",
|
||||||
|
"appSettings_unitsTitle": "Jednotky",
|
||||||
|
"appSettings_unitsMetric": "Metrické (m / km)",
|
||||||
|
"appSettings_unitsImperial": "Imperiálne (ft / mi)",
|
||||||
|
"map_lineOfSight": "Line of Sight",
|
||||||
|
"map_losScreenTitle": "Line of Sight",
|
||||||
|
"losSelectStartEnd": "Vyberte počiatočný a koncový uzol pre LOS.",
|
||||||
|
"losRunFailed": "Kontrola priamej viditeľnosti zlyhala: {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Vymazať všetky body",
|
||||||
|
"losRunToViewElevationProfile": "Ak chcete zobraziť výškový profil, spustite LOS",
|
||||||
|
"losMenuTitle": "Menu LOS",
|
||||||
|
"losMenuSubtitle": "Klepnutím na uzly alebo dlhým stlačením mapy získate vlastné body",
|
||||||
|
"losShowDisplayNodes": "Zobraziť uzly zobrazenia",
|
||||||
|
"losCustomPoints": "Vlastné body",
|
||||||
|
"losCustomPointLabel": "Vlastné {index}",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Bod A",
|
||||||
|
"losPointB": "Bod B",
|
||||||
|
"losAntennaA": "Anténa A: {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Anténa B: {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Spustite LOS",
|
||||||
|
"losNoElevationData": "Žiadne údaje o nadmorskej výške",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, vymazať LOS, min. vôľa {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, blokovaný {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "LOS: kontrolujem...",
|
||||||
|
"losStatusNoData": "LOS: žiadne údaje",
|
||||||
|
"losStatusSummary": "LOS: {clear}/{total} vymazané, {blocked} blokované, {unknown} neznáme",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Údaje o nadmorskej výške nie sú k dispozícii pre jednu alebo viacero vzoriek.",
|
||||||
|
"losErrorInvalidInput": "Neplatné body/údaje o nadmorskej výške pre výpočet LOS.",
|
||||||
|
"losRenameCustomPoint": "Premenovať vlastný bod",
|
||||||
|
"losPointName": "Názov bodu",
|
||||||
|
"losShowPanelTooltip": "Zobraziť panel LOS",
|
||||||
|
"losHidePanelTooltip": "Skryť panel LOS",
|
||||||
|
"losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Rádiový horizont",
|
||||||
|
"losLegendLosBeam": "Priama viditeľnosť",
|
||||||
|
"losLegendTerrain": "Terén",
|
||||||
|
"losFrequencyLabel": "Frekvencia",
|
||||||
|
"losFrequencyInfoTooltip": "Zobraziť podrobnosti výpočtu",
|
||||||
|
"losFrequencyDialogTitle": "Výpočet rádiového horizontu",
|
||||||
|
"losFrequencyDialogDescription": "Počnúc od k={baselineK} pri {baselineFreq} MHz výpočet upraví k-faktor pre aktuálne pásmo {frequencyMHz} MHz, ktorý definuje zakrivený strop rádiového horizontu.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listFilter_removeFromFavorites": "Odstrániť z označení",
|
||||||
|
"listFilter_addToFavorites": "Pridaj do obľúbených",
|
||||||
|
"listFilter_favorites": "Obľúbené",
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_searchRoomServers": "Hľadaj {number}{str} serverov miestností...",
|
||||||
|
"contacts_searchFavorites": "Hľadať {number}{str} obľúbené...",
|
||||||
|
"contacts_searchRepeaters": "Hľadať {number}{str} opakovače...",
|
||||||
|
"contacts_searchUsers": "Hľadať {number}{str} používateľov...",
|
||||||
|
"contacts_searchContactsNoNumber": "Hľadať kontakty...",
|
||||||
|
"contacts_unread": "Neprečítané",
|
||||||
|
"settings_contactSettingsSubtitle": "Nastavenia pre pridávanie kontaktov.",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Automaticky pridávať užívateľov",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Povoliť spoločníkovi automaticky pridávať objavených užívateľov.",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Automaticky pridávať opakovače",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Automaticky pridávať server miestnosti",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Povoliť spoločníkovi automaticky pridať objavené serverové miestnosti.",
|
||||||
|
"contactsSettings_autoAddTitle": "Automatické zisťovanie",
|
||||||
|
"contactsSettings_title": "Nastavenia kontaktov",
|
||||||
|
"contactsSettings_otherTitle": "Ďalšie nastavenia súvisiace s kontaktami",
|
||||||
|
"settings_contactSettings": "Nastavenia kontaktov",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Automaticky pridávať senzory",
|
||||||
|
"discoveredContacts_noMatching": "Žiadne zhodné kontakty",
|
||||||
|
"discoveredContacts_searchHint": "Vyhľadať objavené kontakty",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Povoliť spoločníkovi automaticky pridávať objavené repeater.",
|
||||||
|
"discoveredContacts_contactAdded": "Kontakt bol pridaný",
|
||||||
|
"discoveredContacts_copyContact": "Kopírovať kontakt do schránky",
|
||||||
|
"discoveredContacts_deleteContact": "Zmazať kontakt",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Povoliť spoločníkovi automaticky pridávať objavené senzory.",
|
||||||
|
"discoveredContacts_Title": "Objavené kontakty",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Prepísať najstaršie",
|
||||||
|
"discoveredContacts_addContact": "Pridať kontakt",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Keď je zoznam kontaktov plný, bude nahradený najstarší neoznačený kontakt.",
|
||||||
|
"discoveredContacts_deleteContactAll": "Zmazať všetky objavené kontakty",
|
||||||
|
"common_deleteAll": "Zmazať všetko",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Ste si istí, že chcete zmazať všetky objavené kontakty?",
|
||||||
|
"map_showGuessedLocations": "Zobraziť umiestnenia odhadnutých uzlov",
|
||||||
|
"map_guessedLocation": "Odhadnutá lokalita",
|
||||||
|
"usbScreenTitle": "Pripojte cez USB",
|
||||||
|
"usbScreenSubtitle": "Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vašej MeshCore uzlu.",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenStatus": "Vyberte USB zariadenie",
|
||||||
|
"usbScreenNote": "USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.",
|
||||||
|
"usbScreenEmptyState": "Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte.",
|
||||||
|
"usbErrorPermissionDenied": "Žiadosť o prístup cez USB bola zamietnutá.",
|
||||||
|
"usbErrorDeviceMissing": "Vybrané USB zariadenie už nie je dostupné.",
|
||||||
|
"usbErrorInvalidPort": "Vyberte platné USB zariadenie.",
|
||||||
|
"usbErrorBusy": "Ďalšia požiadavka na pripojenie cez USB je aktuálne v procese.",
|
||||||
|
"usbErrorNotConnected": "Nie je pripojené žiadne USB zariadenie.",
|
||||||
|
"usbErrorOpenFailed": "Nepodarilo sa otvoriť vybrané USB zariadenie.",
|
||||||
|
"usbErrorConnectFailed": "Nepodarilo sa sa sa pripojiť k vybranému USB zariadeniu.",
|
||||||
|
"usbErrorUnsupported": "Podpora USB sériového rozhrania nie je na tejto platforme dostupná.",
|
||||||
|
"usbErrorAlreadyActive": "Pripojenie cez USB je už aktivované.",
|
||||||
|
"usbErrorNoDeviceSelected": "Nebolo vybrané žiadne USB zariadenie.",
|
||||||
|
"usbErrorPortClosed": "Pripojenie cez USB nie je aktivované.",
|
||||||
|
"usbFallbackDeviceName": "Webový sériový zariadenie",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_searching": "Hľadanie USB zariadení...",
|
||||||
|
"usbConnectionFailed": "Neúspešné pripojenie cez USB: {error}",
|
||||||
|
"usbStatus_notConnected": "Vyberte USB zariadenie",
|
||||||
|
"usbStatus_connecting": "Pripojenie k USB zariadeniu...",
|
||||||
|
"usbErrorConnectTimedOut": "Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion."
|
||||||
}
|
}
|
||||||
|
|||||||
+267
-4
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"channels_channelDeleteFailed": "Kanala {name} ni bilo mogoče izbrisati",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@@locale": "sl",
|
"@@locale": "sl",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Stiki",
|
"nav_contacts": "Stiki",
|
||||||
@@ -334,6 +342,8 @@
|
|||||||
"channels_publicChannel": "Javni kanal",
|
"channels_publicChannel": "Javni kanal",
|
||||||
"channels_privateChannel": "Zasebni kanal",
|
"channels_privateChannel": "Zasebni kanal",
|
||||||
"channels_editChannel": "Uredi kanal",
|
"channels_editChannel": "Uredi kanal",
|
||||||
|
"channels_muteChannel": "Utišaj kanal",
|
||||||
|
"channels_unmuteChannel": "Vklopi obvestila kanala",
|
||||||
"channels_deleteChannel": "Pošlji kanal",
|
"channels_deleteChannel": "Pošlji kanal",
|
||||||
"channels_deleteChannelConfirm": "Izbrišem \"{name}\"? To se ne da povrniti.",
|
"channels_deleteChannelConfirm": "Izbrišem \"{name}\"? To se ne da povrniti.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
@@ -1351,12 +1361,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighboursSubtitle": "Pogledati nič sosednjih hopjev.",
|
"repeater_neighborsSubtitle": "Pogledati nič sosednjih hopjev.",
|
||||||
"repeater_neighbours": "Sosedi",
|
"repeater_neighbors": "Sosedi",
|
||||||
"neighbors_receivedData": "Prejeto podatke o sosedih",
|
"neighbors_receivedData": "Prejeto podatke o sosedih",
|
||||||
"neighbors_requestTimedOut": "Sosedi zahtevajo izklop po dogovoru.",
|
"neighbors_requestTimedOut": "Sosedi zahtevajo izklop po dogovoru.",
|
||||||
"neighbors_errorLoading": "Napaka pri obnašanju sosedov: {error}",
|
"neighbors_errorLoading": "Napaka pri obnašanju sosedov: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Ponovitve Sosedi",
|
"neighbors_repeatersNeighbors": "Ponovitve Sosedi",
|
||||||
"neighbors_noData": "Niso na voljo podatki o sosedih.",
|
"neighbors_noData": "Niso na voljo podatki o sosedih.",
|
||||||
"channels_joinPrivateChannel": "Pridružite se zasebni skupini",
|
"channels_joinPrivateChannel": "Pridružite se zasebni skupini",
|
||||||
"channels_createPrivateChannelDesc": "Varno zaklenjeno s skrivnim ključem.",
|
"channels_createPrivateChannelDesc": "Varno zaklenjeno s skrivnim ključem.",
|
||||||
@@ -1550,6 +1560,8 @@
|
|||||||
"contacts_pathTraceTo": "Trace route to {name}",
|
"contacts_pathTraceTo": "Trace route to {name}",
|
||||||
"appSettings_languageRu": "Ruščina",
|
"appSettings_languageRu": "Ruščina",
|
||||||
"appSettings_languageUk": "Ukrajinsko",
|
"appSettings_languageUk": "Ukrajinsko",
|
||||||
|
"appSettings_enableMessageTracing": "Omogoči sledenje sporočilom",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil",
|
||||||
"contacts_contactImported": "Kontakt je bil uvožen.",
|
"contacts_contactImported": "Kontakt je bil uvožen.",
|
||||||
"contacts_contactImportFailed": "Kontakt ni bil uspešno uvožen.",
|
"contacts_contactImportFailed": "Kontakt ni bil uspešno uvožen.",
|
||||||
"contacts_zeroHopAdvert": "Reklama brez posrednikov",
|
"contacts_zeroHopAdvert": "Reklama brez posrednikov",
|
||||||
@@ -1593,8 +1605,259 @@
|
|||||||
"map_pathTraceCancelled": "Spremljanje poti je prekinjeno.",
|
"map_pathTraceCancelled": "Spremljanje poti je prekinjeno.",
|
||||||
"scanner_enableBluetooth": "Omogočite Bluetooth",
|
"scanner_enableBluetooth": "Omogočite Bluetooth",
|
||||||
"scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.",
|
"scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.",
|
||||||
|
"scanner_chromeRequired": "Zahtevan brskalnik Chrome",
|
||||||
|
"scanner_chromeRequiredMessage": "Ta spletna aplikacija za podporo Bluetooth zahteva Google Chrome ali brskalnik na osnovi Chromiuma.",
|
||||||
"scanner_bluetoothOff": "Bluetooth je izklopljen",
|
"scanner_bluetoothOff": "Bluetooth je izklopljen",
|
||||||
|
"snrIndicator_lastSeen": "Zadnjič videno",
|
||||||
|
"snrIndicator_nearByRepeaters": "Bližnji ponovitelji",
|
||||||
|
"chat_ShowAllPaths": "Prikaži vse poti",
|
||||||
"settings_clientRepeatFreqWarning": "Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.",
|
"settings_clientRepeatFreqWarning": "Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.",
|
||||||
"settings_clientRepeatSubtitle": "Omogočite temu naprave, da ponavlja paketne sporočila za druge.",
|
"settings_clientRepeatSubtitle": "Omogočite temu naprave, da ponavlja paketne sporočila za druge.",
|
||||||
"settings_clientRepeat": "Neovadno ponavljanje"
|
"settings_clientRepeat": "Neovadno ponavljanje",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "Podatki o višini LOS: Open-Meteo (CC BY 4.0)",
|
||||||
|
"appSettings_unitsTitle": "Enote",
|
||||||
|
"appSettings_unitsMetric": "Metrična (m/km)",
|
||||||
|
"appSettings_unitsImperial": "Imperialno (ft / mi)",
|
||||||
|
"map_lineOfSight": "Linija vida",
|
||||||
|
"map_losScreenTitle": "Linija vida",
|
||||||
|
"losSelectStartEnd": "Izberite začetno in končno vozlišče za LOS.",
|
||||||
|
"losRunFailed": "Preverjanje vidnega polja ni uspelo: {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Počisti vse točke",
|
||||||
|
"losRunToViewElevationProfile": "Zaženite LOS za ogled višinskega profila",
|
||||||
|
"losMenuTitle": "LOS meni",
|
||||||
|
"losMenuSubtitle": "Tapnite vozlišča ali dolgo pritisnite na zemljevid za točke po meri",
|
||||||
|
"losShowDisplayNodes": "Pokaži prikazna vozlišča",
|
||||||
|
"losCustomPoints": "Točke po meri",
|
||||||
|
"losCustomPointLabel": "Po meri {index}",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Točka A",
|
||||||
|
"losPointB": "Točka B",
|
||||||
|
"losAntennaA": "Antena A: {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Antena B: {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Zaženi LOS",
|
||||||
|
"losNoElevationData": "Ni podatkov o višini",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, čisti LOS, najmanjša razdalja {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, blokiral {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "LOS: preverjam ...",
|
||||||
|
"losStatusNoData": "LOS: ni podatkov",
|
||||||
|
"losStatusSummary": "LOS: {clear}/{total} jasno, {blocked} blokirano, {unknown} neznano",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Podatki o nadmorski višini niso na voljo za enega ali več vzorcev.",
|
||||||
|
"losErrorInvalidInput": "Neveljavni podatki o točkah/višini za izračun LOS.",
|
||||||
|
"losRenameCustomPoint": "Preimenujte točko po meri",
|
||||||
|
"losPointName": "Ime točke",
|
||||||
|
"losShowPanelTooltip": "Pokaži ploščo LOS",
|
||||||
|
"losHidePanelTooltip": "Skrij ploščo LOS",
|
||||||
|
"losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Radijski horizont",
|
||||||
|
"losLegendLosBeam": "Linija vidnosti",
|
||||||
|
"losLegendTerrain": "Teren",
|
||||||
|
"losFrequencyLabel": "Frekvenca",
|
||||||
|
"losFrequencyInfoTooltip": "Prikaži podrobnosti izračuna",
|
||||||
|
"losFrequencyDialogTitle": "Izračun radijskega horizonta",
|
||||||
|
"losFrequencyDialogDescription": "Začenši od k={baselineK} pri {baselineFreq} MHz, izračun prilagodi k-faktor za trenutni pas {frequencyMHz} MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listFilter_favorites": "Priljubljene",
|
||||||
|
"listFilter_removeFromFavorites": "Odstrani iz priljubljenih",
|
||||||
|
"listFilter_addToFavorites": "Dodaj v priljubljene",
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_unread": "Neprebrano",
|
||||||
|
"contacts_searchFavorites": "Iskanje {number}{str} priljubljenih...",
|
||||||
|
"contacts_searchRoomServers": "Išči {number}{str} strežnikov sob...",
|
||||||
|
"contacts_searchContactsNoNumber": "Iskanje stikov...",
|
||||||
|
"contacts_searchRepeaters": "Išči {number}{str} ponavljalnike...",
|
||||||
|
"contacts_searchUsers": "Išči {number}{str} uporabnikov...",
|
||||||
|
"settings_contactSettings": "Nastavitve stika",
|
||||||
|
"contactsSettings_autoAddTitle": "Avtomatsko odkrivanje",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Avtomatsko dodaj uporabnike",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Avtomatsko dodaj ponovitelje",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite ponovitelje.",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Avtomatsko dodaj strežnike sob",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite strežnike sob.",
|
||||||
|
"contactsSettings_otherTitle": "Druge nastavitve v zvezi s stiki",
|
||||||
|
"settings_contactSettingsSubtitle": "Nastavitve za dodajanje stikov.",
|
||||||
|
"contactsSettings_title": "Nastavitve stikov",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Avtomatsko dodaj senzorje",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite uporabnike.",
|
||||||
|
"discoveredContacts_noMatching": "Ni ujemajočih stikov",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite senzorje.",
|
||||||
|
"discoveredContacts_addContact": "Dodaj stik",
|
||||||
|
"discoveredContacts_contactAdded": "Kontakt dodan",
|
||||||
|
"discoveredContacts_copyContact": "Kopiraj stik v odložišče",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Prepiši najstarejše",
|
||||||
|
"discoveredContacts_Title": "Odkriti stiki",
|
||||||
|
"discoveredContacts_searchHint": "Najdeni stiki po iskanju",
|
||||||
|
"discoveredContacts_deleteContact": "Izbriši stik",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Ko je seznam stikov poln, bo najstarejši nestarševski stik zamenjan.",
|
||||||
|
"common_deleteAll": "Izbriši vse",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Ste prepričani, da želite izbrisati vse odkrite kontakte?",
|
||||||
|
"discoveredContacts_deleteContactAll": "Izbriši vse odkrite kontakte",
|
||||||
|
"map_guessedLocation": "Predpostavljena lokacija",
|
||||||
|
"map_showGuessedLocations": "Pokaži lokacije domnevnih not.",
|
||||||
|
"usbScreenTitle": "Povežite preko USB",
|
||||||
|
"usbScreenSubtitle": "Izberite zaznano serijsko napravo in se neposredno povežite z vašo MeshCore napravo.",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenStatus": "Izberite USB naprave",
|
||||||
|
"usbScreenNote": "USB serijska povezava je aktivna na podprtih napravah Android in na desktop platformah.",
|
||||||
|
"usbScreenEmptyState": "Niti en USB naprave niso najdeni. Povežite eno in posodobite.",
|
||||||
|
"usbErrorPermissionDenied": "Dovoljenje za dostop preko USB-ja je bilo zavrnjeno.",
|
||||||
|
"usbErrorDeviceMissing": "Izbrani USB napravej je več ne.",
|
||||||
|
"usbErrorInvalidPort": "Izberite veljavno USB naprave.",
|
||||||
|
"usbErrorBusy": "Že je v teku zahteva za povezavo preko USB.",
|
||||||
|
"usbErrorNotConnected": "Ni priklopljenih USB naprave.",
|
||||||
|
"usbErrorOpenFailed": "Uspešno ni bilo mogo, da se odpre izbran naprave USB.",
|
||||||
|
"usbErrorConnectFailed": "Niso bilo mogoče uskladiti povezave z izbranim USB napom.",
|
||||||
|
"usbErrorUnsupported": "USB serijska komunikacija ni podprta na tej platformi.",
|
||||||
|
"usbErrorAlreadyActive": "USB povezava je že aktivirana.",
|
||||||
|
"usbErrorNoDeviceSelected": "Ni bilo izbranega USB naprave.",
|
||||||
|
"usbErrorPortClosed": "USB povezava ni aktivirana.",
|
||||||
|
"usbFallbackDeviceName": "Naprave za serijsko komunikacijo preko spleta",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_notConnected": "Izberite USB naprave.",
|
||||||
|
"usbStatus_connecting": "Povezava z USB napravo...",
|
||||||
|
"usbStatus_searching": "Iskanje USB naprav...",
|
||||||
|
"usbConnectionFailed": "Napaka pri povezavi preko USB: {error}",
|
||||||
|
"usbErrorConnectTimedOut": "Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion."
|
||||||
}
|
}
|
||||||
|
|||||||
+267
-4
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"channels_channelDeleteFailed": "Det gick inte att ta bort kanalen \"{name}\"",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@@locale": "sv",
|
"@@locale": "sv",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Kontakter",
|
"nav_contacts": "Kontakter",
|
||||||
@@ -334,6 +342,8 @@
|
|||||||
"channels_publicChannel": "Allmänt kanal",
|
"channels_publicChannel": "Allmänt kanal",
|
||||||
"channels_privateChannel": "Privat kanal",
|
"channels_privateChannel": "Privat kanal",
|
||||||
"channels_editChannel": "Redigera kanal",
|
"channels_editChannel": "Redigera kanal",
|
||||||
|
"channels_muteChannel": "Tysta kanal",
|
||||||
|
"channels_unmuteChannel": "Slå på ljud för kanal",
|
||||||
"channels_deleteChannel": "Ta bort kanal",
|
"channels_deleteChannel": "Ta bort kanal",
|
||||||
"channels_deleteChannelConfirm": "Radera \"{name}\"? Detta kan inte ångras.",
|
"channels_deleteChannelConfirm": "Radera \"{name}\"? Detta kan inte ångras.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
@@ -1351,12 +1361,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Grannar",
|
"repeater_neighbors": "Grannar",
|
||||||
"repeater_neighboursSubtitle": "Visa noll hoppgrannar.",
|
"repeater_neighborsSubtitle": "Visa noll hoppgrannar.",
|
||||||
"neighbors_receivedData": "Mottagna grannars data",
|
"neighbors_receivedData": "Mottagna grannars data",
|
||||||
"neighbors_requestTimedOut": "Grannar begär tidsinställd utskick.",
|
"neighbors_requestTimedOut": "Grannar begär tidsinställd utskick.",
|
||||||
"neighbors_errorLoading": "Fel vid inläsning av grannar: {error}",
|
"neighbors_errorLoading": "Fel vid inläsning av grannar: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Upprepar grannar",
|
"neighbors_repeatersNeighbors": "Upprepar grannar",
|
||||||
"neighbors_noData": "Inga grannuppgifter finns tillgängliga.",
|
"neighbors_noData": "Inga grannuppgifter finns tillgängliga.",
|
||||||
"channels_createPrivateChannel": "Skapa en privat kanal",
|
"channels_createPrivateChannel": "Skapa en privat kanal",
|
||||||
"channels_joinPrivateChannel": "Gå med i en Privat Kanal",
|
"channels_joinPrivateChannel": "Gå med i en Privat Kanal",
|
||||||
@@ -1556,6 +1566,8 @@
|
|||||||
"contacts_copyAdvertToClipboard": "Kopiera annons till urklipp",
|
"contacts_copyAdvertToClipboard": "Kopiera annons till urklipp",
|
||||||
"contacts_invalidAdvertFormat": "Ogiltiga kontaktuppgifter",
|
"contacts_invalidAdvertFormat": "Ogiltiga kontaktuppgifter",
|
||||||
"appSettings_languageUk": "Ukrainska",
|
"appSettings_languageUk": "Ukrainska",
|
||||||
|
"appSettings_enableMessageTracing": "Aktivera meddelandespårning",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden",
|
||||||
"contacts_addContactFromClipboard": "Lägg till kontakt från urklipp",
|
"contacts_addContactFromClipboard": "Lägg till kontakt från urklipp",
|
||||||
"contacts_contactImported": "Kontakt har importerats.",
|
"contacts_contactImported": "Kontakt har importerats.",
|
||||||
"contacts_zeroHopContactAdvertSent": "Skickat kontakt via annons.",
|
"contacts_zeroHopContactAdvertSent": "Skickat kontakt via annons.",
|
||||||
@@ -1593,8 +1605,259 @@
|
|||||||
"map_removeLast": "Ta bort sista",
|
"map_removeLast": "Ta bort sista",
|
||||||
"scanner_enableBluetooth": "Aktivera Bluetooth",
|
"scanner_enableBluetooth": "Aktivera Bluetooth",
|
||||||
"scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.",
|
"scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.",
|
||||||
|
"scanner_chromeRequired": "Chrome-webbläsare krävs",
|
||||||
|
"scanner_chromeRequiredMessage": "Denna webbapplikation kräver Google Chrome oder en Chromium-baserader webbläsare för Bluetooth-stöd.",
|
||||||
"scanner_bluetoothOff": "Bluetooth är avstängt",
|
"scanner_bluetoothOff": "Bluetooth är avstängt",
|
||||||
|
"snrIndicator_lastSeen": "Senast sedd",
|
||||||
|
"snrIndicator_nearByRepeaters": "Närliggande uppreparstationer",
|
||||||
|
"chat_ShowAllPaths": "Visa alla vägar",
|
||||||
"settings_clientRepeatSubtitle": "Låt enheten repetera nätpaket för andra användare.",
|
"settings_clientRepeatSubtitle": "Låt enheten repetera nätpaket för andra användare.",
|
||||||
"settings_clientRepeat": "Upprepa utan elnät",
|
"settings_clientRepeat": "Upprepa utan elnät",
|
||||||
"settings_clientRepeatFreqWarning": "För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz."
|
"settings_clientRepeatFreqWarning": "För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz.",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "LOS-höjddata: Open-Meteo (CC BY 4.0)",
|
||||||
|
"appSettings_unitsTitle": "Enheter",
|
||||||
|
"appSettings_unitsMetric": "Metriskt (m/km)",
|
||||||
|
"appSettings_unitsImperial": "Imperialt (ft / mi)",
|
||||||
|
"map_lineOfSight": "Synlinje",
|
||||||
|
"map_losScreenTitle": "Synlinje",
|
||||||
|
"losSelectStartEnd": "Välj start- och slutnoder för LOS.",
|
||||||
|
"losRunFailed": "Synlinjekontroll misslyckades: {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Rensa alla punkter",
|
||||||
|
"losRunToViewElevationProfile": "Kör LOS för att se höjdprofil",
|
||||||
|
"losMenuTitle": "LOS-menyn",
|
||||||
|
"losMenuSubtitle": "Tryck på noder eller tryck länge på kartan för anpassade punkter",
|
||||||
|
"losShowDisplayNodes": "Visa displaynoder",
|
||||||
|
"losCustomPoints": "Anpassade poäng",
|
||||||
|
"losCustomPointLabel": "Anpassad {index}",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Punkt A",
|
||||||
|
"losPointB": "Punkt B",
|
||||||
|
"losAntennaA": "Antenn A: {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Antenn B: {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Kör LOS",
|
||||||
|
"losNoElevationData": "Inga höjddata",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, rensa LOS, min clearance {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, blockerad av {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "LOS: kollar...",
|
||||||
|
"losStatusNoData": "LOS: inga data",
|
||||||
|
"losStatusSummary": "LOS: {clear}/{total} rensa, {blocked} blockerad, {unknown} okänd",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Höjddata är inte tillgänglig för ett eller flera prover.",
|
||||||
|
"losErrorInvalidInput": "Ogiltiga poäng/höjddata för LOS-beräkning.",
|
||||||
|
"losRenameCustomPoint": "Byt namn på anpassad punkt",
|
||||||
|
"losPointName": "Punktnamn",
|
||||||
|
"losShowPanelTooltip": "Visa LOS-panelen",
|
||||||
|
"losHidePanelTooltip": "Dölj LOS-panelen",
|
||||||
|
"losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Radiohorisont",
|
||||||
|
"losLegendLosBeam": "Siktlinje",
|
||||||
|
"losLegendTerrain": "Terräng",
|
||||||
|
"losFrequencyLabel": "Frekvens",
|
||||||
|
"losFrequencyInfoTooltip": "Visa detaljer om beräkningen",
|
||||||
|
"losFrequencyDialogTitle": "Beräkning av radiohorisonten",
|
||||||
|
"losFrequencyDialogDescription": "Med start från k={baselineK} vid {baselineFreq} MHz, justerar beräkningen k-faktorn för det aktuella {frequencyMHz} MHz-bandet, som definierar den böjda radiohorisonten.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listFilter_removeFromFavorites": "Ta bort från favoriter",
|
||||||
|
"listFilter_addToFavorites": "Lägg till i favoriter",
|
||||||
|
"listFilter_favorites": "Favoriter",
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_unread": "Oläst",
|
||||||
|
"contacts_searchContactsNoNumber": "Sök kontakter...",
|
||||||
|
"contacts_searchRepeaters": "Sök {number}{str} upprepningsenheter...",
|
||||||
|
"contacts_searchFavorites": "Sök {number}{str} Favoriter...",
|
||||||
|
"contacts_searchUsers": "Sök {number}{str} användare...",
|
||||||
|
"contacts_searchRoomServers": "Sök {number}{str} Room-servrar...",
|
||||||
|
"settings_contactSettingsSubtitle": "Inställningar för hur kontakter läggs till.",
|
||||||
|
"settings_contactSettings": "Kontaktinställningar",
|
||||||
|
"contactsSettings_autoAddTitle": "Automatisk upptäckt",
|
||||||
|
"contactsSettings_otherTitle": "Andra inställningar relaterade till kontakt",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta användare",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Lägg till upprepande enheter automatiskt",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta rumsservrar.",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Lägg till sensorer automatiskt",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Lägg till användare automatiskt",
|
||||||
|
"contactsSettings_title": "Kontaktinställningar",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta sensorer.",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Skriv över äldst",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta repeater.",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Lägg automatiskt till rumsservrar",
|
||||||
|
"discoveredContacts_noMatching": "Inga matchande kontakter",
|
||||||
|
"discoveredContacts_searchHint": "Sök uppfunna kontakter",
|
||||||
|
"discoveredContacts_deleteContact": "Ta bort kontakt",
|
||||||
|
"discoveredContacts_Title": "Upptäckta kontakter",
|
||||||
|
"discoveredContacts_contactAdded": "Kontakt tillagd",
|
||||||
|
"discoveredContacts_addContact": "Lägg till kontakt",
|
||||||
|
"discoveredContacts_copyContact": "Kopiera kontakt till urklipp",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "När kontaktlistan är full ersätts den äldsta icke-favoriterade kontakten.",
|
||||||
|
"common_deleteAll": "Ta bort alla",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Är du säker på att du vill ta bort alla upptäckta kontakter?",
|
||||||
|
"discoveredContacts_deleteContactAll": "Ta bort alla upptäckta kontakter",
|
||||||
|
"map_guessedLocation": "Gissad plats",
|
||||||
|
"map_showGuessedLocations": "Visa upp de antagna nodernas placeringar",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenSubtitle": "Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.",
|
||||||
|
"usbScreenTitle": "Anslut via USB",
|
||||||
|
"usbScreenStatus": "Välj en USB-enhet",
|
||||||
|
"usbScreenNote": "USB-seriell kommunikation är aktiv på stödda Android-enheter och på skrivbordsplattformar.",
|
||||||
|
"usbScreenEmptyState": "Inga USB-enheter hittades. Anslut en och uppdatera.",
|
||||||
|
"usbErrorPermissionDenied": "Tillgången via USB nekas.",
|
||||||
|
"usbErrorDeviceMissing": "Den valda USB-enheten är inte längre tillgänglig.",
|
||||||
|
"usbErrorInvalidPort": "Välj en giltig USB-enhet.",
|
||||||
|
"usbErrorBusy": "En annan förfrågan om USB-anslutning är redan pågående.",
|
||||||
|
"usbErrorNotConnected": "Ingen USB-enhet är ansluten.",
|
||||||
|
"usbErrorOpenFailed": "Misslyckades med att öppna det valda USB-enheten.",
|
||||||
|
"usbErrorConnectFailed": "Kunde inte ansluta till det valda USB-enheten.",
|
||||||
|
"usbErrorUnsupported": "USB-seriell kommunikation stöds inte på denna plattform.",
|
||||||
|
"usbErrorAlreadyActive": "En USB-anslutning är redan aktiv.",
|
||||||
|
"usbErrorNoDeviceSelected": "Ingen USB-enhet valdes.",
|
||||||
|
"usbErrorPortClosed": "USB-anslutningen är inte aktiv.",
|
||||||
|
"usbFallbackDeviceName": "Web-serieenhet",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_connecting": "Anslutning till USB-enhet...",
|
||||||
|
"usbStatus_notConnected": "Välj en USB-enhet",
|
||||||
|
"usbConnectionFailed": "Fel vid USB-anslutning: {error}",
|
||||||
|
"usbStatus_searching": "Söker efter USB-enheter...",
|
||||||
|
"usbErrorConnectTimedOut": "Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware."
|
||||||
}
|
}
|
||||||
|
|||||||
+267
-4
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"channels_channelDeleteFailed": "Не вдалося видалити канал \"{name}\"",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@@locale": "uk",
|
"@@locale": "uk",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Контакти",
|
"nav_contacts": "Контакти",
|
||||||
@@ -335,6 +343,8 @@
|
|||||||
"channels_publicChannel": "Публічний канал",
|
"channels_publicChannel": "Публічний канал",
|
||||||
"channels_privateChannel": "Приватний канал",
|
"channels_privateChannel": "Приватний канал",
|
||||||
"channels_editChannel": "Редагувати канал",
|
"channels_editChannel": "Редагувати канал",
|
||||||
|
"channels_muteChannel": "Вимкнути сповіщення каналу",
|
||||||
|
"channels_unmuteChannel": "Увімкнути сповіщення каналу",
|
||||||
"channels_deleteChannel": "Видалити канал",
|
"channels_deleteChannel": "Видалити канал",
|
||||||
"channels_deleteChannelConfirm": "Видалити {name}? Це не можна скасувати.",
|
"channels_deleteChannelConfirm": "Видалити {name}? Це не можна скасувати.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
@@ -1352,12 +1362,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Сусіди",
|
"repeater_neighbors": "Сусіди",
|
||||||
"repeater_neighboursSubtitle": "Показати сусідів нульового стрибка.",
|
"repeater_neighborsSubtitle": "Показати сусідів нульового стрибка.",
|
||||||
"neighbors_receivedData": "Дані сусідів отримано",
|
"neighbors_receivedData": "Дані сусідів отримано",
|
||||||
"neighbors_requestTimedOut": "Час запиту сусідів вичерпано.",
|
"neighbors_requestTimedOut": "Час запиту сусідів вичерпано.",
|
||||||
"neighbors_errorLoading": "Помилка завантаження сусідів: {error}",
|
"neighbors_errorLoading": "Помилка завантаження сусідів: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Ретранслятори-сусіди",
|
"neighbors_repeatersNeighbors": "Ретранслятори-сусіди",
|
||||||
"neighbors_noData": "Дані про сусідів недоступні.",
|
"neighbors_noData": "Дані про сусідів недоступні.",
|
||||||
"channels_createPrivateChannelDesc": "Захищено секретним ключем.",
|
"channels_createPrivateChannelDesc": "Захищено секретним ключем.",
|
||||||
"channels_joinPrivateChannel": "Приєднатися до приватного каналу",
|
"channels_joinPrivateChannel": "Приєднатися до приватного каналу",
|
||||||
@@ -1557,6 +1567,8 @@
|
|||||||
"contacts_copyAdvertToClipboard": "Копіювати оголошення в буфер обміну",
|
"contacts_copyAdvertToClipboard": "Копіювати оголошення в буфер обміну",
|
||||||
"contacts_clipboardEmpty": "Буфер обміну порожній",
|
"contacts_clipboardEmpty": "Буфер обміну порожній",
|
||||||
"appSettings_languageRu": "Російська",
|
"appSettings_languageRu": "Російська",
|
||||||
|
"appSettings_enableMessageTracing": "Увімкнути відстеження повідомлень",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Показувати детальні метадані про маршрутизацію та час для повідомлень",
|
||||||
"contacts_ShareContact": "Копіювати контакт у буфер обміну",
|
"contacts_ShareContact": "Копіювати контакт у буфер обміну",
|
||||||
"contacts_zeroHopContactAdvertFailed": "Не вдалося надіслати контакт.",
|
"contacts_zeroHopContactAdvertFailed": "Не вдалося надіслати контакт.",
|
||||||
"contacts_contactAdvertCopied": "Рекламу скопійовано до буфера обміну.",
|
"contacts_contactAdvertCopied": "Рекламу скопійовано до буфера обміну.",
|
||||||
@@ -1593,8 +1605,259 @@
|
|||||||
"map_pathTraceCancelled": "Відмінується трасування шляху",
|
"map_pathTraceCancelled": "Відмінується трасування шляху",
|
||||||
"scanner_enableBluetooth": "Увімкніть Bluetooth",
|
"scanner_enableBluetooth": "Увімкніть Bluetooth",
|
||||||
"scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.",
|
"scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.",
|
||||||
|
"scanner_chromeRequired": "Потрібен браузер Chrome",
|
||||||
|
"scanner_chromeRequiredMessage": "Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.",
|
||||||
"scanner_bluetoothOff": "Bluetooth вимкнено",
|
"scanner_bluetoothOff": "Bluetooth вимкнено",
|
||||||
|
"snrIndicator_lastSeen": "Останній раз бачили",
|
||||||
|
"snrIndicator_nearByRepeaters": "Ближні ретранслятори",
|
||||||
|
"chat_ShowAllPaths": "Показати всі шляхи",
|
||||||
"settings_clientRepeatFreqWarning": "Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.",
|
"settings_clientRepeatFreqWarning": "Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.",
|
||||||
"settings_clientRepeatSubtitle": "Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.",
|
"settings_clientRepeatSubtitle": "Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.",
|
||||||
"settings_clientRepeat": "Автономна система"
|
"settings_clientRepeat": "Автономна система",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "Дані про висоту LOS: Open-Meteo (CC BY 4.0)",
|
||||||
|
"appSettings_unitsTitle": "одиниці",
|
||||||
|
"appSettings_unitsMetric": "Метричний (м / км)",
|
||||||
|
"appSettings_unitsImperial": "Імперська (ft / mi)",
|
||||||
|
"map_lineOfSight": "Пряма видимість",
|
||||||
|
"map_losScreenTitle": "Пряма видимість",
|
||||||
|
"losSelectStartEnd": "Виберіть початковий і кінцевий вузли для LOS.",
|
||||||
|
"losRunFailed": "Помилка перевірки прямої видимості: {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Очистити всі пункти",
|
||||||
|
"losRunToViewElevationProfile": "Запустіть LOS, щоб переглянути профіль висоти",
|
||||||
|
"losMenuTitle": "Меню LOS",
|
||||||
|
"losMenuSubtitle": "Торкніться вузлів або утримуйте карту, щоб отримати власні точки",
|
||||||
|
"losShowDisplayNodes": "Показати вузли відображення",
|
||||||
|
"losCustomPoints": "Користувальницькі точки",
|
||||||
|
"losCustomPointLabel": "Спеціальний {index}",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Точка А",
|
||||||
|
"losPointB": "Точка Б",
|
||||||
|
"losAntennaA": "Антена A: {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Антена B: {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Запустіть LOS",
|
||||||
|
"losNoElevationData": "Немає даних про висоту",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, чистий LOS, мінімальний зазор {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, заблоковано {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "LOS: перевірка...",
|
||||||
|
"losStatusNoData": "LOS: немає даних",
|
||||||
|
"losStatusSummary": "LOS: {clear}/{total} очищено, {blocked} заблоковано, {unknown} невідомо",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Дані про висоту недоступні для одного чи кількох зразків.",
|
||||||
|
"losErrorInvalidInput": "Недійсні дані про точки/висоту для розрахунку LOS.",
|
||||||
|
"losRenameCustomPoint": "Перейменуйте спеціальну точку",
|
||||||
|
"losPointName": "Назва точки",
|
||||||
|
"losShowPanelTooltip": "Показати панель LOS",
|
||||||
|
"losHidePanelTooltip": "Приховати панель LOS",
|
||||||
|
"losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Радіогоризонт",
|
||||||
|
"losLegendLosBeam": "Лінія прямої видимості",
|
||||||
|
"losLegendTerrain": "Рельєф",
|
||||||
|
"losFrequencyLabel": "Частота",
|
||||||
|
"losFrequencyInfoTooltip": "Переглянути деталі розрахунку",
|
||||||
|
"losFrequencyDialogTitle": "Розрахунок радіогоризонту",
|
||||||
|
"losFrequencyDialogDescription": "Починаючи з k={baselineK} на {baselineFreq} МГц, обчислення коригує k-фактор для поточного діапазону {frequencyMHz} МГц, який визначає викривлену межу радіогоризонту.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listFilter_removeFromFavorites": "Видалити зі списку улюблених",
|
||||||
|
"listFilter_addToFavorites": "Додати до улюблених",
|
||||||
|
"listFilter_favorites": "Улюблені",
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_searchRoomServers": "Пошук {number}{str} серверів кімнат...",
|
||||||
|
"contacts_searchUsers": "Пошук {number}{str} користувачів...",
|
||||||
|
"contacts_searchFavorites": "Пошук {number}{str} улюблених...",
|
||||||
|
"contacts_searchContactsNoNumber": "Пошук контактів...",
|
||||||
|
"contacts_searchRepeaters": "Пошук {number}{str} ретрансляторів...",
|
||||||
|
"contacts_unread": "Непрочитане",
|
||||||
|
"settings_contactSettingsSubtitle": "Налаштування для додавання контактів",
|
||||||
|
"settings_contactSettings": "Налаштування контактів",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Дозволити супутникові автоматично додавати виявлених користувачів",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Автоматично додавати повторювачі",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Дозволити супутнику автоматично додавати виявлені ретранслятори",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Автоматично додавати сервери кімнат",
|
||||||
|
"contactsSettings_otherTitle": "Інші налаштування, пов'язані з контактами",
|
||||||
|
"contactsSettings_autoAddTitle": "Автоматичне виявлення",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Автоматично додавати користувачів",
|
||||||
|
"contactsSettings_title": "Налаштування контактів",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Дозволити супровіднику автоматично додавати виявлені сервери кімнат.",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Автоматично додавати датчики",
|
||||||
|
"discoveredContacts_searchHint": "Знайти виявлені контакти",
|
||||||
|
"discoveredContacts_contactAdded": "Контакт додано",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Дозволити супровіднику автоматично додавати виявлені сенсори",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Перезаписати найстаріше",
|
||||||
|
"discoveredContacts_Title": "Виявлені контакти",
|
||||||
|
"discoveredContacts_noMatching": "Відповідних контактів не знайдено",
|
||||||
|
"discoveredContacts_deleteContact": "Видалити контакт",
|
||||||
|
"discoveredContacts_copyContact": "Копіювати контакт у буфер обміну",
|
||||||
|
"discoveredContacts_addContact": "Додати контакт",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Коли список контактів заповнений, найстарший контакт без позначки улюбленого буде замінений.",
|
||||||
|
"common_deleteAll": "Видалити все",
|
||||||
|
"discoveredContacts_deleteContactAll": "Видалити всі виявлені контакти",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Ви впевнені, що хочете видалити всі виявлені контакти?",
|
||||||
|
"map_showGuessedLocations": "Показати місцезнаходження передбачених вузлів",
|
||||||
|
"map_guessedLocation": "Визначено місцезнаходження",
|
||||||
|
"usbScreenSubtitle": "Виберіть виявлене серійне пристрій і підключіть його безпосередньо до вашого вузла MeshCore.",
|
||||||
|
"usbScreenTitle": "Підключити через USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenStatus": "Виберіть пристрій USB",
|
||||||
|
"usbScreenNote": "USB-серіальний інтерфейс активний на підтримуваних пристроях на базі Android та на десктопних платформах.",
|
||||||
|
"usbScreenEmptyState": "Не знайдено жодних пристроїв USB. Підключіть один і перезавантажте.",
|
||||||
|
"usbErrorPermissionDenied": "Було відмовлено у наданні дозволу на використання USB.",
|
||||||
|
"usbErrorDeviceMissing": "Вибране USB-пристрій більше недоступне.",
|
||||||
|
"usbErrorInvalidPort": "Виберіть дійсний USB-пристрій.",
|
||||||
|
"usbErrorBusy": "Ще один запит на підключення через USB вже обробляється.",
|
||||||
|
"usbErrorNotConnected": "Немає підключених пристроїв USB.",
|
||||||
|
"usbErrorOpenFailed": "Не вдалося відкрити вибране USB-пристрій.",
|
||||||
|
"usbErrorConnectFailed": "Не вдалося підключитися до вибраного USB-пристрою.",
|
||||||
|
"usbErrorUnsupported": "Підтримка USB-серіального інтерфейсу не реалізована на цій платформі.",
|
||||||
|
"usbErrorAlreadyActive": "USB-з'єднання вже встановлено.",
|
||||||
|
"usbErrorNoDeviceSelected": "Не було вибрано жодного пристрою USB.",
|
||||||
|
"usbErrorPortClosed": "З'єднання USB не встановлено.",
|
||||||
|
"usbFallbackDeviceName": "Пристрій для передачі даних по веб-серіалах",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_searching": "Пошук пристроїв USB...",
|
||||||
|
"usbStatus_notConnected": "Виберіть пристрій USB",
|
||||||
|
"usbConnectionFailed": "Не вдалося встановити з'єднання через USB: {error}",
|
||||||
|
"usbStatus_connecting": "Підключення до USB-пристрою...",
|
||||||
|
"usbErrorConnectTimedOut": "З'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion."
|
||||||
}
|
}
|
||||||
|
|||||||
+753
-485
File diff suppressed because it is too large
Load Diff
+37
-1
@@ -1,8 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'l10n/app_localizations.dart';
|
import 'l10n/app_localizations.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'screens/chrome_required_screen.dart';
|
||||||
|
import 'utils/platform_info.dart';
|
||||||
|
|
||||||
import 'connector/meshcore_connector.dart';
|
import 'connector/meshcore_connector.dart';
|
||||||
import 'screens/scanner_screen.dart';
|
import 'screens/scanner_screen.dart';
|
||||||
import 'services/storage_service.dart';
|
import 'services/storage_service.dart';
|
||||||
@@ -14,6 +18,7 @@ import 'services/ble_debug_log_service.dart';
|
|||||||
import 'services/app_debug_log_service.dart';
|
import 'services/app_debug_log_service.dart';
|
||||||
import 'services/background_service.dart';
|
import 'services/background_service.dart';
|
||||||
import 'services/map_tile_cache_service.dart';
|
import 'services/map_tile_cache_service.dart';
|
||||||
|
import 'services/chat_text_scale_service.dart';
|
||||||
import 'storage/prefs_manager.dart';
|
import 'storage/prefs_manager.dart';
|
||||||
import 'utils/app_logger.dart';
|
import 'utils/app_logger.dart';
|
||||||
|
|
||||||
@@ -33,6 +38,7 @@ void main() async {
|
|||||||
final appDebugLogService = AppDebugLogService();
|
final appDebugLogService = AppDebugLogService();
|
||||||
final backgroundService = BackgroundService();
|
final backgroundService = BackgroundService();
|
||||||
final mapTileCacheService = MapTileCacheService();
|
final mapTileCacheService = MapTileCacheService();
|
||||||
|
final chatTextScaleService = ChatTextScaleService();
|
||||||
|
|
||||||
// Load settings
|
// Load settings
|
||||||
await appSettingsService.loadSettings();
|
await appSettingsService.loadSettings();
|
||||||
@@ -47,6 +53,9 @@ void main() async {
|
|||||||
final notificationService = NotificationService();
|
final notificationService = NotificationService();
|
||||||
await notificationService.initialize();
|
await notificationService.initialize();
|
||||||
await backgroundService.initialize();
|
await backgroundService.initialize();
|
||||||
|
_registerThirdPartyLicenses();
|
||||||
|
|
||||||
|
await chatTextScaleService.initialize();
|
||||||
|
|
||||||
// Wire up connector with services
|
// Wire up connector with services
|
||||||
connector.initialize(
|
connector.initialize(
|
||||||
@@ -76,10 +85,32 @@ void main() async {
|
|||||||
bleDebugLogService: bleDebugLogService,
|
bleDebugLogService: bleDebugLogService,
|
||||||
appDebugLogService: appDebugLogService,
|
appDebugLogService: appDebugLogService,
|
||||||
mapTileCacheService: mapTileCacheService,
|
mapTileCacheService: mapTileCacheService,
|
||||||
|
chatTextScaleService: chatTextScaleService,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _registerThirdPartyLicenses() {
|
||||||
|
LicenseRegistry.addLicense(() async* {
|
||||||
|
yield const LicenseEntryWithLineBreaks(
|
||||||
|
<String>['Open-Meteo Elevation API Data'],
|
||||||
|
'''
|
||||||
|
Data used by LOS elevation lookups is provided by Open-Meteo.
|
||||||
|
|
||||||
|
Open-Meteo terms and attribution:
|
||||||
|
https://open-meteo.com/en/terms
|
||||||
|
|
||||||
|
Elevation API:
|
||||||
|
https://open-meteo.com/en/docs/elevation-api
|
||||||
|
|
||||||
|
Attribution license reference:
|
||||||
|
Creative Commons Attribution 4.0 International (CC BY 4.0)
|
||||||
|
https://creativecommons.org/licenses/by/4.0/
|
||||||
|
''',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class MeshCoreApp extends StatelessWidget {
|
class MeshCoreApp extends StatelessWidget {
|
||||||
final MeshCoreConnector connector;
|
final MeshCoreConnector connector;
|
||||||
final MessageRetryService retryService;
|
final MessageRetryService retryService;
|
||||||
@@ -89,6 +120,7 @@ class MeshCoreApp extends StatelessWidget {
|
|||||||
final BleDebugLogService bleDebugLogService;
|
final BleDebugLogService bleDebugLogService;
|
||||||
final AppDebugLogService appDebugLogService;
|
final AppDebugLogService appDebugLogService;
|
||||||
final MapTileCacheService mapTileCacheService;
|
final MapTileCacheService mapTileCacheService;
|
||||||
|
final ChatTextScaleService chatTextScaleService;
|
||||||
|
|
||||||
const MeshCoreApp({
|
const MeshCoreApp({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -100,6 +132,7 @@ class MeshCoreApp extends StatelessWidget {
|
|||||||
required this.bleDebugLogService,
|
required this.bleDebugLogService,
|
||||||
required this.appDebugLogService,
|
required this.appDebugLogService,
|
||||||
required this.mapTileCacheService,
|
required this.mapTileCacheService,
|
||||||
|
required this.chatTextScaleService,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -112,6 +145,7 @@ class MeshCoreApp extends StatelessWidget {
|
|||||||
ChangeNotifierProvider.value(value: appSettingsService),
|
ChangeNotifierProvider.value(value: appSettingsService),
|
||||||
ChangeNotifierProvider.value(value: bleDebugLogService),
|
ChangeNotifierProvider.value(value: bleDebugLogService),
|
||||||
ChangeNotifierProvider.value(value: appDebugLogService),
|
ChangeNotifierProvider.value(value: appDebugLogService),
|
||||||
|
ChangeNotifierProvider.value(value: chatTextScaleService),
|
||||||
Provider.value(value: storage),
|
Provider.value(value: storage),
|
||||||
Provider.value(value: mapTileCacheService),
|
Provider.value(value: mapTileCacheService),
|
||||||
],
|
],
|
||||||
@@ -156,7 +190,9 @@ class MeshCoreApp extends StatelessWidget {
|
|||||||
NotificationService().setLocale(locale);
|
NotificationService().setLocale(locale);
|
||||||
return child ?? const SizedBox.shrink();
|
return child ?? const SizedBox.shrink();
|
||||||
},
|
},
|
||||||
home: const ScannerScreen(),
|
home: (PlatformInfo.isWeb && !PlatformInfo.isChrome)
|
||||||
|
? const ChromeRequiredScreen()
|
||||||
|
: const ScannerScreen(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,3 +1,16 @@
|
|||||||
|
enum UnitSystem { metric, imperial }
|
||||||
|
|
||||||
|
extension UnitSystemValue on UnitSystem {
|
||||||
|
String get value {
|
||||||
|
switch (this) {
|
||||||
|
case UnitSystem.imperial:
|
||||||
|
return 'imperial';
|
||||||
|
case UnitSystem.metric:
|
||||||
|
return 'metric';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class AppSettings {
|
class AppSettings {
|
||||||
static const Object _unset = Object();
|
static const Object _unset = Object();
|
||||||
|
|
||||||
@@ -9,6 +22,8 @@ class AppSettings {
|
|||||||
final bool mapKeyPrefixEnabled;
|
final bool mapKeyPrefixEnabled;
|
||||||
final String mapKeyPrefix;
|
final String mapKeyPrefix;
|
||||||
final bool mapShowMarkers;
|
final bool mapShowMarkers;
|
||||||
|
final bool mapShowGuessedLocations;
|
||||||
|
final bool enableMessageTracing;
|
||||||
final Map<String, double>? mapCacheBounds;
|
final Map<String, double>? mapCacheBounds;
|
||||||
final int mapCacheMinZoom;
|
final int mapCacheMinZoom;
|
||||||
final int mapCacheMaxZoom;
|
final int mapCacheMaxZoom;
|
||||||
@@ -21,6 +36,9 @@ class AppSettings {
|
|||||||
final String? languageOverride; // null = system default
|
final String? languageOverride; // null = system default
|
||||||
final bool appDebugLogEnabled;
|
final bool appDebugLogEnabled;
|
||||||
final Map<String, String> batteryChemistryByDeviceId;
|
final Map<String, String> batteryChemistryByDeviceId;
|
||||||
|
final Map<String, String> batteryChemistryByRepeaterId;
|
||||||
|
final UnitSystem unitSystem;
|
||||||
|
final Set<String> mutedChannels;
|
||||||
|
|
||||||
AppSettings({
|
AppSettings({
|
||||||
this.clearPathOnMaxRetry = false,
|
this.clearPathOnMaxRetry = false,
|
||||||
@@ -31,6 +49,8 @@ class AppSettings {
|
|||||||
this.mapKeyPrefixEnabled = false,
|
this.mapKeyPrefixEnabled = false,
|
||||||
this.mapKeyPrefix = '',
|
this.mapKeyPrefix = '',
|
||||||
this.mapShowMarkers = true,
|
this.mapShowMarkers = true,
|
||||||
|
this.mapShowGuessedLocations = true,
|
||||||
|
this.enableMessageTracing = false,
|
||||||
this.mapCacheBounds,
|
this.mapCacheBounds,
|
||||||
this.mapCacheMinZoom = 10,
|
this.mapCacheMinZoom = 10,
|
||||||
this.mapCacheMaxZoom = 15,
|
this.mapCacheMaxZoom = 15,
|
||||||
@@ -43,7 +63,12 @@ class AppSettings {
|
|||||||
this.languageOverride,
|
this.languageOverride,
|
||||||
this.appDebugLogEnabled = false,
|
this.appDebugLogEnabled = false,
|
||||||
Map<String, String>? batteryChemistryByDeviceId,
|
Map<String, String>? batteryChemistryByDeviceId,
|
||||||
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {};
|
Map<String, String>? batteryChemistryByRepeaterId,
|
||||||
|
this.unitSystem = UnitSystem.metric,
|
||||||
|
Set<String>? mutedChannels,
|
||||||
|
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
|
||||||
|
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
|
||||||
|
mutedChannels = mutedChannels ?? {};
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
@@ -55,6 +80,8 @@ class AppSettings {
|
|||||||
'map_key_prefix_enabled': mapKeyPrefixEnabled,
|
'map_key_prefix_enabled': mapKeyPrefixEnabled,
|
||||||
'map_key_prefix': mapKeyPrefix,
|
'map_key_prefix': mapKeyPrefix,
|
||||||
'map_show_markers': mapShowMarkers,
|
'map_show_markers': mapShowMarkers,
|
||||||
|
'map_show_guessed_locations': mapShowGuessedLocations,
|
||||||
|
'enable_message_tracing': enableMessageTracing,
|
||||||
'map_cache_bounds': mapCacheBounds,
|
'map_cache_bounds': mapCacheBounds,
|
||||||
'map_cache_min_zoom': mapCacheMinZoom,
|
'map_cache_min_zoom': mapCacheMinZoom,
|
||||||
'map_cache_max_zoom': mapCacheMaxZoom,
|
'map_cache_max_zoom': mapCacheMaxZoom,
|
||||||
@@ -67,10 +94,20 @@ class AppSettings {
|
|||||||
'language_override': languageOverride,
|
'language_override': languageOverride,
|
||||||
'app_debug_log_enabled': appDebugLogEnabled,
|
'app_debug_log_enabled': appDebugLogEnabled,
|
||||||
'battery_chemistry_by_device_id': batteryChemistryByDeviceId,
|
'battery_chemistry_by_device_id': batteryChemistryByDeviceId,
|
||||||
|
'battery_chemistry_by_repeater_id': batteryChemistryByRepeaterId,
|
||||||
|
'unit_system': unitSystem.value,
|
||||||
|
'muted_channels': mutedChannels.toList(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
factory AppSettings.fromJson(Map<String, dynamic> json) {
|
factory AppSettings.fromJson(Map<String, dynamic> json) {
|
||||||
|
UnitSystem parseUnitSystem(dynamic value) {
|
||||||
|
if (value is String && value.toLowerCase() == 'imperial') {
|
||||||
|
return UnitSystem.imperial;
|
||||||
|
}
|
||||||
|
return UnitSystem.metric;
|
||||||
|
}
|
||||||
|
|
||||||
return AppSettings(
|
return AppSettings(
|
||||||
clearPathOnMaxRetry: json['clear_path_on_max_retry'] as bool? ?? false,
|
clearPathOnMaxRetry: json['clear_path_on_max_retry'] as bool? ?? false,
|
||||||
mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true,
|
mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true,
|
||||||
@@ -81,6 +118,9 @@ class AppSettings {
|
|||||||
mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false,
|
mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false,
|
||||||
mapKeyPrefix: json['map_key_prefix'] as String? ?? '',
|
mapKeyPrefix: json['map_key_prefix'] as String? ?? '',
|
||||||
mapShowMarkers: json['map_show_markers'] as bool? ?? true,
|
mapShowMarkers: json['map_show_markers'] as bool? ?? true,
|
||||||
|
mapShowGuessedLocations:
|
||||||
|
json['map_show_guessed_locations'] as bool? ?? true,
|
||||||
|
enableMessageTracing: json['enable_message_tracing'] as bool? ?? false,
|
||||||
mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map(
|
mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map(
|
||||||
(key, value) => MapEntry(key.toString(), (value as num).toDouble()),
|
(key, value) => MapEntry(key.toString(), (value as num).toDouble()),
|
||||||
),
|
),
|
||||||
@@ -101,6 +141,17 @@ class AppSettings {
|
|||||||
(key, value) => MapEntry(key.toString(), value.toString()),
|
(key, value) => MapEntry(key.toString(), value.toString()),
|
||||||
) ??
|
) ??
|
||||||
{},
|
{},
|
||||||
|
batteryChemistryByRepeaterId:
|
||||||
|
(json['battery_chemistry_by_repeater_id'] as Map?)?.map(
|
||||||
|
(key, value) => MapEntry(key.toString(), value.toString()),
|
||||||
|
) ??
|
||||||
|
{},
|
||||||
|
unitSystem: parseUnitSystem(json['unit_system']),
|
||||||
|
mutedChannels:
|
||||||
|
((json['muted_channels'] as List?)
|
||||||
|
?.map((e) => e.toString())
|
||||||
|
.toSet()) ??
|
||||||
|
{},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +164,8 @@ class AppSettings {
|
|||||||
bool? mapKeyPrefixEnabled,
|
bool? mapKeyPrefixEnabled,
|
||||||
String? mapKeyPrefix,
|
String? mapKeyPrefix,
|
||||||
bool? mapShowMarkers,
|
bool? mapShowMarkers,
|
||||||
|
bool? mapShowGuessedLocations,
|
||||||
|
bool? enableMessageTracing,
|
||||||
Object? mapCacheBounds = _unset,
|
Object? mapCacheBounds = _unset,
|
||||||
int? mapCacheMinZoom,
|
int? mapCacheMinZoom,
|
||||||
int? mapCacheMaxZoom,
|
int? mapCacheMaxZoom,
|
||||||
@@ -125,6 +178,9 @@ class AppSettings {
|
|||||||
Object? languageOverride = _unset,
|
Object? languageOverride = _unset,
|
||||||
bool? appDebugLogEnabled,
|
bool? appDebugLogEnabled,
|
||||||
Map<String, String>? batteryChemistryByDeviceId,
|
Map<String, String>? batteryChemistryByDeviceId,
|
||||||
|
Map<String, String>? batteryChemistryByRepeaterId,
|
||||||
|
UnitSystem? unitSystem,
|
||||||
|
Set<String>? mutedChannels,
|
||||||
}) {
|
}) {
|
||||||
return AppSettings(
|
return AppSettings(
|
||||||
clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry,
|
clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry,
|
||||||
@@ -135,6 +191,9 @@ class AppSettings {
|
|||||||
mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled,
|
mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled,
|
||||||
mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix,
|
mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix,
|
||||||
mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers,
|
mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers,
|
||||||
|
mapShowGuessedLocations:
|
||||||
|
mapShowGuessedLocations ?? this.mapShowGuessedLocations,
|
||||||
|
enableMessageTracing: enableMessageTracing ?? this.enableMessageTracing,
|
||||||
mapCacheBounds: mapCacheBounds == _unset
|
mapCacheBounds: mapCacheBounds == _unset
|
||||||
? this.mapCacheBounds
|
? this.mapCacheBounds
|
||||||
: mapCacheBounds as Map<String, double>?,
|
: mapCacheBounds as Map<String, double>?,
|
||||||
@@ -154,6 +213,10 @@ class AppSettings {
|
|||||||
appDebugLogEnabled: appDebugLogEnabled ?? this.appDebugLogEnabled,
|
appDebugLogEnabled: appDebugLogEnabled ?? this.appDebugLogEnabled,
|
||||||
batteryChemistryByDeviceId:
|
batteryChemistryByDeviceId:
|
||||||
batteryChemistryByDeviceId ?? this.batteryChemistryByDeviceId,
|
batteryChemistryByDeviceId ?? this.batteryChemistryByDeviceId,
|
||||||
|
batteryChemistryByRepeaterId:
|
||||||
|
batteryChemistryByRepeaterId ?? this.batteryChemistryByRepeaterId,
|
||||||
|
unitSystem: unitSystem ?? this.unitSystem,
|
||||||
|
mutedChannels: mutedChannels ?? this.mutedChannels,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-34
@@ -5,6 +5,7 @@ class Contact {
|
|||||||
final Uint8List publicKey;
|
final Uint8List publicKey;
|
||||||
final String name;
|
final String name;
|
||||||
final int type;
|
final int type;
|
||||||
|
final int flags;
|
||||||
final int pathLength; // -1 = flood, 0+ = direct hops (from device)
|
final int pathLength; // -1 = flood, 0+ = direct hops (from device)
|
||||||
final Uint8List path; // Path bytes from device
|
final Uint8List path; // Path bytes from device
|
||||||
final int?
|
final int?
|
||||||
@@ -19,6 +20,7 @@ class Contact {
|
|||||||
required this.publicKey,
|
required this.publicKey,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.type,
|
required this.type,
|
||||||
|
this.flags = 0,
|
||||||
required this.pathLength,
|
required this.pathLength,
|
||||||
required this.path,
|
required this.path,
|
||||||
this.pathOverride,
|
this.pathOverride,
|
||||||
@@ -58,11 +60,13 @@ class Contact {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool get hasLocation => latitude != null && longitude != null;
|
bool get hasLocation => latitude != null && longitude != null;
|
||||||
|
bool get isFavorite => (flags & contactFlagFavorite) != 0;
|
||||||
|
|
||||||
Contact copyWith({
|
Contact copyWith({
|
||||||
Uint8List? publicKey,
|
Uint8List? publicKey,
|
||||||
String? name,
|
String? name,
|
||||||
int? type,
|
int? type,
|
||||||
|
int? flags,
|
||||||
int? pathLength,
|
int? pathLength,
|
||||||
Uint8List? path,
|
Uint8List? path,
|
||||||
int? pathOverride,
|
int? pathOverride,
|
||||||
@@ -77,6 +81,7 @@ class Contact {
|
|||||||
publicKey: publicKey ?? this.publicKey,
|
publicKey: publicKey ?? this.publicKey,
|
||||||
name: name ?? this.name,
|
name: name ?? this.name,
|
||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
|
flags: flags ?? this.flags,
|
||||||
pathLength: pathLength ?? this.pathLength,
|
pathLength: pathLength ?? this.pathLength,
|
||||||
path: path ?? this.path,
|
path: path ?? this.path,
|
||||||
pathOverride: clearPathOverride
|
pathOverride: clearPathOverride
|
||||||
@@ -119,7 +124,7 @@ class Contact {
|
|||||||
final pathBytes = _pathBytesForDisplay;
|
final pathBytes = _pathBytesForDisplay;
|
||||||
Uint8List? traceBytes;
|
Uint8List? traceBytes;
|
||||||
|
|
||||||
if (pathLength <= 0) {
|
if (pathBytes.isEmpty) {
|
||||||
traceBytes = Uint8List(1);
|
traceBytes = Uint8List(1);
|
||||||
traceBytes[0] = publicKey[0];
|
traceBytes[0] = publicKey[0];
|
||||||
return traceBytes;
|
return traceBytes;
|
||||||
@@ -160,43 +165,49 @@ class Contact {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Contact? fromFrame(Uint8List data) {
|
static Contact? fromFrame(Uint8List data) {
|
||||||
if (data.length < contactFrameSize) return null;
|
if (data.isEmpty) return null;
|
||||||
if (data[0] != respCodeContact) return null;
|
if (data[0] != respCodeContact) return null;
|
||||||
|
try {
|
||||||
|
final pubKey = Uint8List.fromList(
|
||||||
|
data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize),
|
||||||
|
);
|
||||||
|
final type = data[contactTypeOffset];
|
||||||
|
final flags = data[contactFlagsOffset];
|
||||||
|
final pathLen = data[contactPathLenOffset].toSigned(8);
|
||||||
|
final safePathLen = pathLen > 0
|
||||||
|
? (pathLen > maxPathSize ? maxPathSize : pathLen)
|
||||||
|
: 0;
|
||||||
|
final pathBytes = safePathLen > 0
|
||||||
|
? Uint8List.fromList(
|
||||||
|
data.sublist(contactPathOffset, contactPathOffset + safePathLen),
|
||||||
|
)
|
||||||
|
: Uint8List(0);
|
||||||
|
final name = readCString(data, contactNameOffset, maxNameSize);
|
||||||
|
final lastmod = readUint32LE(data, contactLastModOffset);
|
||||||
|
|
||||||
final pubKey = Uint8List.fromList(
|
double? lat, lon;
|
||||||
data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize),
|
final latRaw = readInt32LE(data, contactLatOffset);
|
||||||
);
|
final lonRaw = readInt32LE(data, contactLonOffset);
|
||||||
final type = data[contactTypeOffset];
|
if (latRaw != 0 || lonRaw != 0) {
|
||||||
final pathLen = data[contactPathLenOffset].toSigned(8);
|
lat = latRaw / 1e6;
|
||||||
final safePathLen = pathLen > 0
|
lon = lonRaw / 1e6;
|
||||||
? (pathLen > maxPathSize ? maxPathSize : pathLen)
|
}
|
||||||
: 0;
|
|
||||||
final pathBytes = safePathLen > 0
|
|
||||||
? Uint8List.fromList(
|
|
||||||
data.sublist(contactPathOffset, contactPathOffset + safePathLen),
|
|
||||||
)
|
|
||||||
: Uint8List(0);
|
|
||||||
final name = readCString(data, contactNameOffset, maxNameSize);
|
|
||||||
final lastmod = readUint32LE(data, contactLastmodOffset);
|
|
||||||
|
|
||||||
double? lat, lon;
|
return Contact(
|
||||||
final latRaw = readInt32LE(data, contactLatOffset);
|
publicKey: pubKey,
|
||||||
final lonRaw = readInt32LE(data, contactLonOffset);
|
name: name.isEmpty ? 'Unknown' : name,
|
||||||
if (latRaw != 0 || lonRaw != 0) {
|
type: type,
|
||||||
lat = latRaw / 1e6;
|
flags: flags,
|
||||||
lon = lonRaw / 1e6;
|
pathLength: pathLen,
|
||||||
|
path: pathBytes,
|
||||||
|
latitude: lat,
|
||||||
|
longitude: lon,
|
||||||
|
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// If parsing fails, return null
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Contact(
|
|
||||||
publicKey: pubKey,
|
|
||||||
name: name.isEmpty ? 'Unknown' : name,
|
|
||||||
type: type,
|
|
||||||
pathLength: pathLen,
|
|
||||||
path: pathBytes,
|
|
||||||
latitude: lat,
|
|
||||||
longitude: lon,
|
|
||||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
import '../connector/meshcore_protocol.dart';
|
||||||
|
|
||||||
|
class DiscoveryContact {
|
||||||
|
final Uint8List rawPacket;
|
||||||
|
final Uint8List publicKey;
|
||||||
|
final String name;
|
||||||
|
final int type;
|
||||||
|
final int pathLength; // -1 = flood, 0+ = direct hops (from device)
|
||||||
|
final Uint8List path; // Path bytes from device
|
||||||
|
final double? latitude;
|
||||||
|
final double? longitude;
|
||||||
|
final DateTime lastSeen;
|
||||||
|
|
||||||
|
DiscoveryContact({
|
||||||
|
required this.rawPacket,
|
||||||
|
required this.publicKey,
|
||||||
|
required this.name,
|
||||||
|
required this.type,
|
||||||
|
required this.pathLength,
|
||||||
|
required this.path,
|
||||||
|
this.latitude,
|
||||||
|
this.longitude,
|
||||||
|
required this.lastSeen,
|
||||||
|
});
|
||||||
|
|
||||||
|
String get publicKeyHex => pubKeyToHex(publicKey);
|
||||||
|
|
||||||
|
String get typeLabel {
|
||||||
|
switch (type) {
|
||||||
|
case advTypeChat:
|
||||||
|
return 'Chat';
|
||||||
|
case advTypeRepeater:
|
||||||
|
return 'Repeater';
|
||||||
|
case advTypeRoom:
|
||||||
|
return 'Room';
|
||||||
|
case advTypeSensor:
|
||||||
|
return 'Sensor';
|
||||||
|
default:
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String get pathLabel {
|
||||||
|
if (pathLength < 0) return 'Flood';
|
||||||
|
if (pathLength == 0) return 'Direct';
|
||||||
|
return '$pathLength hops';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get hasLocation => latitude != null && longitude != null;
|
||||||
|
|
||||||
|
DiscoveryContact copyWith({
|
||||||
|
Uint8List? rawPacket,
|
||||||
|
Uint8List? publicKey,
|
||||||
|
String? name,
|
||||||
|
int? type,
|
||||||
|
int? pathLength,
|
||||||
|
Uint8List? path,
|
||||||
|
double? latitude,
|
||||||
|
double? longitude,
|
||||||
|
DateTime? lastSeen,
|
||||||
|
}) {
|
||||||
|
return DiscoveryContact(
|
||||||
|
rawPacket: rawPacket ?? this.rawPacket,
|
||||||
|
publicKey: publicKey ?? this.publicKey,
|
||||||
|
name: name ?? this.name,
|
||||||
|
type: type ?? this.type,
|
||||||
|
pathLength: pathLength ?? this.pathLength,
|
||||||
|
path: path ?? this.path,
|
||||||
|
latitude: latitude ?? this.latitude,
|
||||||
|
longitude: longitude ?? this.longitude,
|
||||||
|
lastSeen: lastSeen ?? this.lastSeen,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String get pathIdList {
|
||||||
|
final pathBytes = path;
|
||||||
|
if (pathBytes.isEmpty) return '';
|
||||||
|
final parts = <String>[];
|
||||||
|
final groupSize = pathHashSize;
|
||||||
|
for (int i = 0; i < pathBytes.length; i += groupSize) {
|
||||||
|
final end = (i + groupSize) <= pathBytes.length
|
||||||
|
? (i + groupSize)
|
||||||
|
: pathBytes.length;
|
||||||
|
final chunk = pathBytes.sublist(i, end);
|
||||||
|
parts.add(
|
||||||
|
chunk
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||||
|
.join(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return parts.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
String get shortPubKeyHex {
|
||||||
|
return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>";
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
other is DiscoveryContact && publicKeyHex == other.publicKeyHex;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => publicKeyHex.hashCode;
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../services/app_debug_log_service.dart';
|
import '../services/app_debug_log_service.dart';
|
||||||
|
import '../widgets/adaptive_app_bar_title.dart';
|
||||||
|
|
||||||
class AppDebugLogScreen extends StatelessWidget {
|
class AppDebugLogScreen extends StatelessWidget {
|
||||||
const AppDebugLogScreen({super.key});
|
const AppDebugLogScreen({super.key});
|
||||||
@@ -17,7 +18,7 @@ class AppDebugLogScreen extends StatelessWidget {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(context.l10n.debugLog_appTitle),
|
title: AdaptiveAppBarTitle(context.l10n.debugLog_appTitle),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
|
import '../models/app_settings.dart';
|
||||||
import '../services/app_settings_service.dart';
|
import '../services/app_settings_service.dart';
|
||||||
import '../services/notification_service.dart';
|
import '../services/notification_service.dart';
|
||||||
|
import '../widgets/adaptive_app_bar_title.dart';
|
||||||
import 'map_cache_screen.dart';
|
import 'map_cache_screen.dart';
|
||||||
|
|
||||||
class AppSettingsScreen extends StatelessWidget {
|
class AppSettingsScreen extends StatelessWidget {
|
||||||
@@ -14,7 +16,7 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(context.l10n.appSettings_title),
|
title: AdaptiveAppBarTitle(context.l10n.appSettings_title),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
@@ -80,6 +82,18 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
onTap: () => _showLanguageDialog(context, settingsService),
|
onTap: () => _showLanguageDialog(context, settingsService),
|
||||||
),
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
SwitchListTile(
|
||||||
|
secondary: const Icon(Icons.location_searching),
|
||||||
|
title: Text(context.l10n.appSettings_enableMessageTracing),
|
||||||
|
subtitle: Text(
|
||||||
|
context.l10n.appSettings_enableMessageTracingSubtitle,
|
||||||
|
),
|
||||||
|
value: settingsService.settings.enableMessageTracing,
|
||||||
|
onChanged: (value) {
|
||||||
|
settingsService.setEnableMessageTracing(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -360,6 +374,18 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
onTap: () => _showTimeFilterDialog(context, settingsService),
|
onTap: () => _showTimeFilterDialog(context, settingsService),
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.straighten),
|
||||||
|
title: Text(context.l10n.appSettings_unitsTitle),
|
||||||
|
subtitle: Text(
|
||||||
|
settingsService.settings.unitSystem == UnitSystem.imperial
|
||||||
|
? context.l10n.appSettings_unitsImperial
|
||||||
|
: context.l10n.appSettings_unitsMetric,
|
||||||
|
),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () => _showUnitsDialog(context, settingsService),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.download_outlined),
|
leading: const Icon(Icons.download_outlined),
|
||||||
title: Text(context.l10n.appSettings_offlineMapCache),
|
title: Text(context.l10n.appSettings_offlineMapCache),
|
||||||
@@ -706,6 +732,46 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showUnitsDialog(
|
||||||
|
BuildContext context,
|
||||||
|
AppSettingsService settingsService,
|
||||||
|
) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text(context.l10n.appSettings_unitsTitle),
|
||||||
|
content: RadioGroup<UnitSystem>(
|
||||||
|
groupValue: settingsService.settings.unitSystem,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
settingsService.setUnitSystem(value);
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
title: Text(context.l10n.appSettings_unitsMetric),
|
||||||
|
leading: const Radio<UnitSystem>(value: UnitSystem.metric),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(context.l10n.appSettings_unitsImperial),
|
||||||
|
leading: const Radio<UnitSystem>(value: UnitSystem.imperial),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(context.l10n.common_close),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildDebugCard(
|
Widget _buildDebugCard(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
AppSettingsService settingsService,
|
AppSettingsService settingsService,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
|
|||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../services/ble_debug_log_service.dart';
|
import '../services/ble_debug_log_service.dart';
|
||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
|
import '../widgets/adaptive_app_bar_title.dart';
|
||||||
|
|
||||||
enum _BleLogView { frames, rawLogRx }
|
enum _BleLogView { frames, rawLogRx }
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
|||||||
: rawEntries.isNotEmpty;
|
: rawEntries.isNotEmpty;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(context.l10n.debugLog_bleTitle),
|
title: AdaptiveAppBarTitle(context.l10n.debugLog_bleTitle),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: context.l10n.debugLog_copyLog,
|
tooltip: context.l10n.debugLog_copyLog,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
@@ -16,11 +17,15 @@ import '../helpers/utf8_length_limiter.dart';
|
|||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../models/channel.dart';
|
import '../models/channel.dart';
|
||||||
import '../models/channel_message.dart';
|
import '../models/channel_message.dart';
|
||||||
|
import '../services/app_settings_service.dart';
|
||||||
|
import '../services/chat_text_scale_service.dart';
|
||||||
import '../utils/emoji_utils.dart';
|
import '../utils/emoji_utils.dart';
|
||||||
|
import '../widgets/chat_zoom_wrapper.dart';
|
||||||
import '../widgets/emoji_picker.dart';
|
import '../widgets/emoji_picker.dart';
|
||||||
import '../widgets/gif_message.dart';
|
import '../widgets/gif_message.dart';
|
||||||
import '../widgets/jump_to_bottom_button.dart';
|
import '../widgets/jump_to_bottom_button.dart';
|
||||||
import '../widgets/gif_picker.dart';
|
import '../widgets/gif_picker.dart';
|
||||||
|
import '../widgets/message_status_icon.dart';
|
||||||
import 'channel_message_path_screen.dart';
|
import 'channel_message_path_screen.dart';
|
||||||
import 'map_screen.dart';
|
import 'map_screen.dart';
|
||||||
|
|
||||||
@@ -216,37 +221,50 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
ListView.builder(
|
ChatZoomWrapper(
|
||||||
reverse: true, // List grows from bottom up
|
child: ListView.builder(
|
||||||
controller: _scrollController,
|
reverse: true, // List grows from bottom up
|
||||||
padding: const EdgeInsets.all(8),
|
controller: _scrollController,
|
||||||
itemCount: itemCount,
|
padding: const EdgeInsets.all(8),
|
||||||
itemBuilder: (context, index) {
|
itemCount: itemCount,
|
||||||
// Loading indicator now appears at end (bottom) of reversed list
|
itemBuilder: (context, index) {
|
||||||
if (_isLoadingOlder && index == itemCount - 1) {
|
// Loading indicator now appears at end (bottom) of reversed list
|
||||||
return const Padding(
|
if (_isLoadingOlder && index == itemCount - 1) {
|
||||||
padding: EdgeInsets.symmetric(vertical: 16),
|
return const Padding(
|
||||||
child: Center(
|
padding: EdgeInsets.symmetric(vertical: 16),
|
||||||
child: SizedBox(
|
child: Center(
|
||||||
width: 20,
|
child: SizedBox(
|
||||||
height: 20,
|
width: 20,
|
||||||
child: CircularProgressIndicator(
|
height: 20,
|
||||||
strokeWidth: 2,
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final messageIndex = index;
|
||||||
|
final message = reversedMessages[messageIndex];
|
||||||
|
if (!_messageKeys.containsKey(message.messageId)) {
|
||||||
|
_messageKeys[message.messageId] = GlobalKey();
|
||||||
|
}
|
||||||
|
return Container(
|
||||||
|
key: _messageKeys[message.messageId]!,
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final textScale = context
|
||||||
|
.select<ChatTextScaleService, double>(
|
||||||
|
(service) => service.scale,
|
||||||
|
);
|
||||||
|
return _buildMessageBubble(
|
||||||
|
message,
|
||||||
|
textScale,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
final messageIndex = index;
|
),
|
||||||
final message = reversedMessages[messageIndex];
|
|
||||||
if (!_messageKeys.containsKey(message.messageId)) {
|
|
||||||
_messageKeys[message.messageId] = GlobalKey();
|
|
||||||
}
|
|
||||||
return Container(
|
|
||||||
key: _messageKeys[message.messageId]!,
|
|
||||||
child: _buildMessageBubble(message),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
JumpToBottomButton(scrollController: _scrollController),
|
JumpToBottomButton(scrollController: _scrollController),
|
||||||
],
|
],
|
||||||
@@ -261,7 +279,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMessageBubble(ChannelMessage message) {
|
Widget _buildMessageBubble(ChannelMessage message, double textScale) {
|
||||||
|
final settingsService = context.watch<AppSettingsService>();
|
||||||
|
final enableTracing = settingsService.settings.enableMessageTracing;
|
||||||
final isOutgoing = message.isOutgoing;
|
final isOutgoing = message.isOutgoing;
|
||||||
final gifId = _parseGifId(message.text);
|
final gifId = _parseGifId(message.text);
|
||||||
final poi = _parsePoiMessage(message.text);
|
final poi = _parsePoiMessage(message.text);
|
||||||
@@ -271,107 +291,184 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
? message.pathVariants.first
|
? message.pathVariants.first
|
||||||
: Uint8List(0));
|
: Uint8List(0));
|
||||||
|
|
||||||
return Padding(
|
const maxSwipeOffset = 64.0;
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
const replySwipeThreshold = 64.0;
|
||||||
child: Column(
|
const bodyFontSize = 14.0;
|
||||||
crossAxisAlignment: isOutgoing
|
final messageBody = Column(
|
||||||
? CrossAxisAlignment.end
|
crossAxisAlignment: isOutgoing
|
||||||
: CrossAxisAlignment.start,
|
? CrossAxisAlignment.end
|
||||||
children: [
|
: CrossAxisAlignment.start,
|
||||||
Row(
|
children: [
|
||||||
mainAxisAlignment: isOutgoing
|
Row(
|
||||||
? MainAxisAlignment.end
|
mainAxisAlignment: isOutgoing
|
||||||
: MainAxisAlignment.start,
|
? MainAxisAlignment.end
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
: MainAxisAlignment.start,
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
if (!isOutgoing) ...[
|
children: [
|
||||||
_buildAvatar(message.senderName),
|
if (!isOutgoing) ...[
|
||||||
const SizedBox(width: 8),
|
_buildAvatar(message.senderName),
|
||||||
],
|
const SizedBox(width: 8),
|
||||||
Flexible(
|
],
|
||||||
child: GestureDetector(
|
Flexible(
|
||||||
onTap: () => _showMessagePathInfo(message),
|
child: GestureDetector(
|
||||||
onLongPress: () => _showMessageActions(message),
|
onTap: () => _showMessagePathInfo(message),
|
||||||
child: Container(
|
onLongPress: () => _showMessageActions(message),
|
||||||
padding: gifId != null
|
child: Container(
|
||||||
? const EdgeInsets.all(4)
|
padding: gifId != null
|
||||||
: const EdgeInsets.symmetric(
|
? const EdgeInsets.all(4)
|
||||||
horizontal: 12,
|
: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
vertical: 8,
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: MediaQuery.of(context).size.width * 0.65,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isOutgoing
|
||||||
|
? Theme.of(context).colorScheme.primaryContainer
|
||||||
|
: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (!isOutgoing) ...[
|
||||||
|
Padding(
|
||||||
|
padding: gifId != null
|
||||||
|
? const EdgeInsets.only(
|
||||||
|
left: 8,
|
||||||
|
top: 4,
|
||||||
|
bottom: 4,
|
||||||
|
)
|
||||||
|
: EdgeInsets.zero,
|
||||||
|
child: Text(
|
||||||
|
message.senderName,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
constraints: BoxConstraints(
|
),
|
||||||
maxWidth: MediaQuery.of(context).size.width * 0.65,
|
if (gifId == null) const SizedBox(height: 4),
|
||||||
),
|
],
|
||||||
decoration: BoxDecoration(
|
if (message.replyToMessageId != null) ...[
|
||||||
color: isOutgoing
|
_buildReplyPreview(message, textScale),
|
||||||
? Theme.of(context).colorScheme.primaryContainer
|
const SizedBox(height: 8),
|
||||||
: Theme.of(
|
],
|
||||||
context,
|
if (poi != null)
|
||||||
).colorScheme.surfaceContainerHighest,
|
_buildPoiMessage(
|
||||||
borderRadius: BorderRadius.circular(12),
|
context,
|
||||||
),
|
poi,
|
||||||
child: Column(
|
isOutgoing,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
textScale,
|
||||||
children: [
|
trailing: (!enableTracing && isOutgoing)
|
||||||
if (!isOutgoing) ...[
|
? Padding(
|
||||||
Padding(
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
padding: gifId != null
|
child: MessageStatusIcon(
|
||||||
? const EdgeInsets.only(
|
isAcked:
|
||||||
left: 8,
|
message.status ==
|
||||||
top: 4,
|
ChannelMessageStatus.sent &&
|
||||||
bottom: 4,
|
displayPath.isNotEmpty,
|
||||||
)
|
isFailed:
|
||||||
: EdgeInsets.zero,
|
message.status ==
|
||||||
child: Text(
|
ChannelMessageStatus.failed,
|
||||||
message.senderName,
|
),
|
||||||
style: TextStyle(
|
)
|
||||||
fontSize: 12,
|
: null,
|
||||||
fontWeight: FontWeight.bold,
|
)
|
||||||
color: Theme.of(context).colorScheme.primary,
|
else if (gifId != null)
|
||||||
|
Stack(
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: GifMessage(
|
||||||
|
url:
|
||||||
|
'https://media.giphy.com/media/$gifId/giphy.gif',
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
fallbackTextColor: isOutgoing
|
||||||
|
? Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onPrimaryContainer
|
||||||
|
.withValues(alpha: 0.7)
|
||||||
|
: Theme.of(context).colorScheme.onSurface
|
||||||
|
.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (!enableTracing && isOutgoing)
|
||||||
if (gifId == null) const SizedBox(height: 4),
|
Positioned(
|
||||||
],
|
top: 0,
|
||||||
if (message.replyToMessageId != null) ...[
|
right: 0,
|
||||||
_buildReplyPreview(message),
|
child: Container(
|
||||||
const SizedBox(height: 8),
|
padding: const EdgeInsets.all(3),
|
||||||
],
|
decoration: BoxDecoration(
|
||||||
if (poi != null)
|
color: isOutgoing
|
||||||
_buildPoiMessage(context, poi, isOutgoing)
|
? Theme.of(
|
||||||
else if (gifId != null)
|
context,
|
||||||
ClipRRect(
|
).colorScheme.primaryContainer
|
||||||
borderRadius: BorderRadius.circular(8),
|
: Theme.of(
|
||||||
child: GifMessage(
|
context,
|
||||||
url:
|
).colorScheme.surfaceContainerHighest,
|
||||||
'https://media.giphy.com/media/$gifId/giphy.gif',
|
borderRadius: const BorderRadius.only(
|
||||||
backgroundColor: Colors.transparent,
|
bottomLeft: Radius.circular(10),
|
||||||
fallbackTextColor: isOutgoing
|
topRight: Radius.circular(8),
|
||||||
? Theme.of(context)
|
),
|
||||||
.colorScheme
|
),
|
||||||
.onPrimaryContainer
|
child: MessageStatusIcon(
|
||||||
.withValues(alpha: 0.7)
|
isAcked:
|
||||||
: Theme.of(context).colorScheme.onSurface
|
message.status ==
|
||||||
.withValues(alpha: 0.6),
|
ChannelMessageStatus.sent &&
|
||||||
|
displayPath.isNotEmpty,
|
||||||
|
isFailed:
|
||||||
|
message.status ==
|
||||||
|
ChannelMessageStatus.failed,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Linkify(
|
||||||
|
text: message.text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: bodyFontSize * textScale,
|
||||||
|
),
|
||||||
|
linkStyle: TextStyle(
|
||||||
|
fontSize: bodyFontSize * textScale,
|
||||||
|
color: Colors.green,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
options: const LinkifyOptions(
|
||||||
|
humanize: false,
|
||||||
|
defaultToHttps: false,
|
||||||
|
),
|
||||||
|
linkifiers: const [UrlLinkifier()],
|
||||||
|
onOpen: (link) => LinkHandler.handleLinkTap(
|
||||||
|
context,
|
||||||
|
link.url,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
if (!enableTracing && isOutgoing) ...[
|
||||||
else
|
const SizedBox(width: 4),
|
||||||
Linkify(
|
Padding(
|
||||||
text: message.text,
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
style: const TextStyle(fontSize: 14),
|
child: MessageStatusIcon(
|
||||||
linkStyle: const TextStyle(
|
isAcked:
|
||||||
fontSize: 14,
|
message.status ==
|
||||||
color: Colors.green,
|
ChannelMessageStatus.sent &&
|
||||||
decoration: TextDecoration.underline,
|
displayPath.isNotEmpty,
|
||||||
),
|
isFailed:
|
||||||
options: const LinkifyOptions(
|
message.status ==
|
||||||
humanize: false,
|
ChannelMessageStatus.failed,
|
||||||
defaultToHttps: false,
|
),
|
||||||
),
|
),
|
||||||
linkifiers: const [UrlLinkifier()],
|
],
|
||||||
onOpen: (link) =>
|
],
|
||||||
LinkHandler.handleLinkTap(context, link.url),
|
),
|
||||||
),
|
if (enableTracing) ...[
|
||||||
if (displayPath.isNotEmpty) ...[
|
if (displayPath.isNotEmpty) ...[
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Padding(
|
Padding(
|
||||||
@@ -443,25 +540,81 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
if (message.reactions.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(left: isOutgoing ? 0 : 48),
|
|
||||||
child: _buildReactionsDisplay(message),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
if (message.reactions.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(left: isOutgoing ? 0 : 48),
|
||||||
|
child: _buildReactionsDisplay(message),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isOutgoing) {
|
||||||
|
return _SwipeReplyBubble(
|
||||||
|
maxSwipeOffset: maxSwipeOffset,
|
||||||
|
replySwipeThreshold: replySwipeThreshold,
|
||||||
|
onReplyTriggered: () => _setReplyingTo(message),
|
||||||
|
hintBuilder: ({required isStart}) =>
|
||||||
|
_buildReplySwipeHint(isStart: isStart),
|
||||||
|
child: messageBody,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: messageBody,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildReplySwipeHint({required bool isStart}) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
final content = Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.reply, color: colorScheme.primary),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
context.l10n.chat_reply,
|
||||||
|
style: TextStyle(
|
||||||
|
color: colorScheme.primary,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
alignment: isStart ? Alignment.centerLeft : Alignment.centerRight,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
color: colorScheme.primary.withValues(alpha: 0.08),
|
||||||
|
child: isStart
|
||||||
|
? content
|
||||||
|
: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
context.l10n.chat_reply,
|
||||||
|
style: TextStyle(
|
||||||
|
color: colorScheme.primary,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Icon(Icons.reply, color: colorScheme.primary),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildReplyPreview(ChannelMessage message) {
|
Widget _buildReplyPreview(ChannelMessage message, double textScale) {
|
||||||
final connector = context.read<MeshCoreConnector>();
|
final connector = context.read<MeshCoreConnector>();
|
||||||
final isOwnNode = message.replyToSenderName == connector.selfName;
|
final isOwnNode = message.replyToSenderName == connector.selfName;
|
||||||
final replyText = message.replyToText ?? '';
|
final replyText = message.replyToText ?? '';
|
||||||
@@ -489,7 +642,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
context.l10n.chat_location,
|
context.l10n.chat_location,
|
||||||
style: TextStyle(fontSize: 12, color: previewTextColor),
|
style: TextStyle(fontSize: 12 * textScale, color: previewTextColor),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -499,7 +652,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12 * textScale,
|
||||||
color: previewTextColor,
|
color: previewTextColor,
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
),
|
),
|
||||||
@@ -523,7 +676,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
Text(
|
Text(
|
||||||
context.l10n.chat_replyTo(message.replyToSenderName ?? ''),
|
context.l10n.chat_replyTo(message.replyToSenderName ?? ''),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11 * textScale,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: isOwnNode
|
color: isOwnNode
|
||||||
? Theme.of(context).colorScheme.primary
|
? Theme.of(context).colorScheme.primary
|
||||||
@@ -599,7 +752,13 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
return _PoiInfo(lat: lat, lon: lon, label: label);
|
return _PoiInfo(lat: lat, lon: lon, label: label);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPoiMessage(BuildContext context, _PoiInfo poi, bool isOutgoing) {
|
Widget _buildPoiMessage(
|
||||||
|
BuildContext context,
|
||||||
|
_PoiInfo poi,
|
||||||
|
bool isOutgoing,
|
||||||
|
double textScale, {
|
||||||
|
Widget? trailing,
|
||||||
|
}) {
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
final textColor = isOutgoing
|
final textColor = isOutgoing
|
||||||
? colorScheme.onPrimaryContainer
|
? colorScheme.onPrimaryContainer
|
||||||
@@ -635,16 +794,21 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
context.l10n.chat_poiShared,
|
context.l10n.chat_poiShared,
|
||||||
style: TextStyle(color: textColor, fontWeight: FontWeight.w600),
|
style: TextStyle(
|
||||||
|
color: textColor,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 14 * textScale,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (poi.label.isNotEmpty)
|
if (poi.label.isNotEmpty)
|
||||||
Text(
|
Text(
|
||||||
poi.label,
|
poi.label,
|
||||||
style: TextStyle(color: metaColor, fontSize: 12),
|
style: TextStyle(color: metaColor, fontSize: 12 * textScale),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (trailing != null) ...[const SizedBox(width: 4), trailing],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -709,7 +873,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
return colors[hash.abs() % colors.length];
|
return colors[hash.abs() % colors.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildReplyBanner() {
|
Widget _buildReplyBanner(double textScale) {
|
||||||
final message = _replyingToMessage!;
|
final message = _replyingToMessage!;
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@@ -735,7 +899,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
Text(
|
Text(
|
||||||
context.l10n.chat_replyingTo(message.senderName),
|
context.l10n.chat_replyingTo(message.senderName),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12 * textScale,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||||
),
|
),
|
||||||
@@ -745,7 +909,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11 * textScale,
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
).colorScheme.onSecondaryContainer.withValues(alpha: 0.7),
|
).colorScheme.onSecondaryContainer.withValues(alpha: 0.7),
|
||||||
@@ -772,7 +936,15 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (_replyingToMessage != null) _buildReplyBanner(),
|
if (_replyingToMessage != null)
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final textScale = context.select<ChatTextScaleService, double>(
|
||||||
|
(service) => service.scale,
|
||||||
|
);
|
||||||
|
return _buildReplyBanner(textScale);
|
||||||
|
},
|
||||||
|
),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -798,30 +970,47 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
builder: (context, value, child) {
|
builder: (context, value, child) {
|
||||||
final gifId = _parseGifId(value.text);
|
final gifId = _parseGifId(value.text);
|
||||||
if (gifId != null) {
|
if (gifId != null) {
|
||||||
return Row(
|
return Focus(
|
||||||
children: [
|
autofocus: true,
|
||||||
Expanded(
|
onKeyEvent: (node, event) {
|
||||||
child: ClipRRect(
|
if (event is KeyDownEvent &&
|
||||||
borderRadius: BorderRadius.circular(12),
|
(event.logicalKey == LogicalKeyboardKey.enter ||
|
||||||
child: GifMessage(
|
event.logicalKey ==
|
||||||
url:
|
LogicalKeyboardKey.numpadEnter)) {
|
||||||
'https://media.giphy.com/media/$gifId/giphy.gif',
|
_sendMessage();
|
||||||
backgroundColor: Theme.of(
|
return KeyEventResult.handled;
|
||||||
context,
|
}
|
||||||
).colorScheme.surfaceContainerHighest,
|
return KeyEventResult.ignored;
|
||||||
fallbackTextColor: Theme.of(
|
},
|
||||||
context,
|
child: Row(
|
||||||
).colorScheme.onSurface.withValues(alpha: 0.6),
|
children: [
|
||||||
maxSize: 160,
|
Expanded(
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: GifMessage(
|
||||||
|
url:
|
||||||
|
'https://media.giphy.com/media/$gifId/giphy.gif',
|
||||||
|
backgroundColor: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.surfaceContainerHighest,
|
||||||
|
fallbackTextColor: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withValues(alpha: 0.6),
|
||||||
|
maxSize: 160,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 8),
|
||||||
const SizedBox(width: 8),
|
IconButton(
|
||||||
IconButton(
|
icon: const Icon(Icons.close),
|
||||||
icon: const Icon(Icons.close),
|
onPressed: () {
|
||||||
onPressed: () => _textController.clear(),
|
_textController.clear();
|
||||||
),
|
_textFieldFocusNode.requestFocus();
|
||||||
],
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -884,6 +1073,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
connector.sendChannelMessage(widget.channel, messageText);
|
connector.sendChannelMessage(widget.channel, messageText);
|
||||||
_textController.clear();
|
_textController.clear();
|
||||||
_cancelReply();
|
_cancelReply();
|
||||||
|
_textFieldFocusNode.requestFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatTime(DateTime time) {
|
String _formatTime(DateTime time) {
|
||||||
@@ -901,7 +1091,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => ChannelMessagePathScreen(message: message),
|
builder: (context) =>
|
||||||
|
ChannelMessagePathScreen(message: message, channelMessage: true),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1006,6 +1197,157 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _SwipeReplyBubble extends StatefulWidget {
|
||||||
|
final double maxSwipeOffset;
|
||||||
|
final double replySwipeThreshold;
|
||||||
|
final VoidCallback onReplyTriggered;
|
||||||
|
final Widget Function({required bool isStart}) hintBuilder;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const _SwipeReplyBubble({
|
||||||
|
required this.maxSwipeOffset,
|
||||||
|
required this.replySwipeThreshold,
|
||||||
|
required this.onReplyTriggered,
|
||||||
|
required this.hintBuilder,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_SwipeReplyBubble> createState() => _SwipeReplyBubbleState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> {
|
||||||
|
Offset? _swipeStartPosition;
|
||||||
|
double _swipeOffset = 0;
|
||||||
|
double _maxSwipeDistance = 0;
|
||||||
|
int? _swipePointerId;
|
||||||
|
bool _swipeLockedToHorizontal = false;
|
||||||
|
|
||||||
|
void _handleSwipeStart(Offset position) {
|
||||||
|
_swipeStartPosition = position;
|
||||||
|
_maxSwipeDistance = 0;
|
||||||
|
if (_swipeOffset != 0) {
|
||||||
|
setState(() => _swipeOffset = 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSwipePointerDown(PointerDownEvent event) {
|
||||||
|
_swipePointerId = event.pointer;
|
||||||
|
_swipeLockedToHorizontal = false;
|
||||||
|
_handleSwipeStart(event.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSwipePointerMove(PointerMoveEvent event) {
|
||||||
|
if (_swipePointerId != event.pointer || _swipeStartPosition == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final dx = event.position.dx - _swipeStartPosition!.dx;
|
||||||
|
|
||||||
|
const axisLockThreshold = 12.0;
|
||||||
|
if (!_swipeLockedToHorizontal) {
|
||||||
|
if (-dx < axisLockThreshold) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_swipeLockedToHorizontal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleSwipeUpdate(event.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSwipeUpdate(Offset position) {
|
||||||
|
if (_swipeStartPosition == null) return;
|
||||||
|
|
||||||
|
final dx = position.dx - _swipeStartPosition!.dx;
|
||||||
|
if (dx >= 0) return;
|
||||||
|
|
||||||
|
if (-dx < 6) return;
|
||||||
|
|
||||||
|
if (-dx > _maxSwipeDistance) {
|
||||||
|
_maxSwipeDistance = -dx;
|
||||||
|
}
|
||||||
|
|
||||||
|
final double clamped = dx.clamp(-widget.maxSwipeOffset, 0.0).toDouble();
|
||||||
|
final adjusted = _applySwipeResistance(clamped, widget.maxSwipeOffset);
|
||||||
|
if (adjusted != _swipeOffset) {
|
||||||
|
setState(() => _swipeOffset = adjusted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSwipePointerUp(Offset position) {
|
||||||
|
if (_swipeLockedToHorizontal && _swipeStartPosition != null) {
|
||||||
|
final dx = position.dx - _swipeStartPosition!.dx;
|
||||||
|
final peak = math.max(
|
||||||
|
_maxSwipeDistance,
|
||||||
|
(-dx).clamp(0.0, double.infinity),
|
||||||
|
);
|
||||||
|
if (peak >= widget.replySwipeThreshold) {
|
||||||
|
widget.onReplyTriggered();
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_resetSwipe();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resetSwipe() {
|
||||||
|
if (_swipeOffset != 0) {
|
||||||
|
setState(() => _swipeOffset = 0);
|
||||||
|
}
|
||||||
|
_swipeStartPosition = null;
|
||||||
|
_maxSwipeDistance = 0;
|
||||||
|
_swipePointerId = null;
|
||||||
|
_swipeLockedToHorizontal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
double _applySwipeResistance(double rawOffset, double maxOffset) {
|
||||||
|
final abs = rawOffset.abs();
|
||||||
|
if (abs <= 0) return 0;
|
||||||
|
final norm = (abs / maxOffset).clamp(0.0, 1.0);
|
||||||
|
const deadZone = 0.18;
|
||||||
|
if (norm <= deadZone) {
|
||||||
|
return rawOffset.sign * maxOffset * (norm * 0.08);
|
||||||
|
}
|
||||||
|
final t = ((norm - deadZone) / (1 - deadZone)).clamp(0.0, 1.0);
|
||||||
|
final curved = t < 0.5
|
||||||
|
? 16 * math.pow(t, 5)
|
||||||
|
: 1 - math.pow(-2 * t + 2, 5) / 2;
|
||||||
|
const deadZoneEnd = 0.0144;
|
||||||
|
return rawOffset.sign *
|
||||||
|
maxOffset *
|
||||||
|
(deadZoneEnd + curved * (1 - deadZoneEnd));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Listener(
|
||||||
|
onPointerDown: _handleSwipePointerDown,
|
||||||
|
onPointerMove: _handleSwipePointerMove,
|
||||||
|
onPointerUp: (event) => _handleSwipePointerUp(event.position),
|
||||||
|
onPointerCancel: (_) => _resetSwipe(),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: _swipeOffset.abs() / widget.maxSwipeOffset,
|
||||||
|
child: widget.hintBuilder(isStart: false),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
transform: Matrix4.translationValues(_swipeOffset, 0, 0),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _PoiInfo {
|
class _PoiInfo {
|
||||||
final double lat;
|
final double lat;
|
||||||
final double lon;
|
final double lon;
|
||||||
|
|||||||
@@ -9,26 +9,38 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
import '../services/map_tile_cache_service.dart';
|
import '../services/map_tile_cache_service.dart';
|
||||||
|
import '../services/app_settings_service.dart';
|
||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../models/channel_message.dart';
|
import '../models/channel_message.dart';
|
||||||
|
import '../models/app_settings.dart';
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
|
import '../widgets/adaptive_app_bar_title.dart';
|
||||||
|
|
||||||
class ChannelMessagePathScreen extends StatelessWidget {
|
class ChannelMessagePathScreen extends StatelessWidget {
|
||||||
final ChannelMessage message;
|
final ChannelMessage message;
|
||||||
|
final bool channelMessage;
|
||||||
const ChannelMessagePathScreen({super.key, required this.message});
|
const ChannelMessagePathScreen({
|
||||||
|
super.key,
|
||||||
|
required this.message,
|
||||||
|
this.channelMessage = false,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<MeshCoreConnector>(
|
return Consumer<MeshCoreConnector>(
|
||||||
builder: (context, connector, _) {
|
builder: (context, connector, _) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
final primaryPath = _selectPrimaryPath(
|
final primaryPathTmp = _selectPrimaryPath(
|
||||||
message.pathBytes,
|
message.pathBytes,
|
||||||
message.pathVariants,
|
message.pathVariants,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final primaryPath = !channelMessage && !message.isOutgoing
|
||||||
|
? Uint8List.fromList(primaryPathTmp.reversed.toList())
|
||||||
|
: primaryPathTmp;
|
||||||
|
|
||||||
final hops = _buildPathHops(primaryPath, connector.contacts, l10n);
|
final hops = _buildPathHops(primaryPath, connector.contacts, l10n);
|
||||||
final hasHopDetails = primaryPath.isNotEmpty;
|
final hasHopDetails = primaryPath.isNotEmpty;
|
||||||
final observedLabel = _formatObservedHops(
|
final observedLabel = _formatObservedHops(
|
||||||
@@ -37,10 +49,9 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
l10n,
|
l10n,
|
||||||
);
|
);
|
||||||
final extraPaths = _otherPaths(primaryPath, message.pathVariants);
|
final extraPaths = _otherPaths(primaryPath, message.pathVariants);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(l10n.channelPath_title),
|
title: AdaptiveAppBarTitle(l10n.channelPath_title),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.radar_outlined),
|
icon: const Icon(Icons.radar_outlined),
|
||||||
@@ -50,9 +61,9 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => PathTraceMapScreen(
|
builder: (context) => PathTraceMapScreen(
|
||||||
title: context.l10n.contacts_repeaterPathTrace,
|
title: context.l10n.contacts_repeaterPathTrace,
|
||||||
path: Uint8List.fromList(primaryPath),
|
path: primaryPath,
|
||||||
flipPathRound: true,
|
flipPathRound: true,
|
||||||
reversePathRound: true,
|
reversePathRound: !message.isOutgoing && !channelMessage,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -62,7 +73,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
tooltip: l10n.channelPath_viewMap,
|
tooltip: l10n.channelPath_viewMap,
|
||||||
onPressed: hasHopDetails
|
onPressed: hasHopDetails
|
||||||
? () {
|
? () {
|
||||||
_openPathMap(context);
|
_openPathMap(context, channelMessage: channelMessage);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@@ -157,7 +168,11 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
subtitle: Text(_formatPathPrefixes(variants[i])),
|
subtitle: Text(_formatPathPrefixes(variants[i])),
|
||||||
trailing: const Icon(Icons.map_outlined, size: 20),
|
trailing: const Icon(Icons.map_outlined, size: 20),
|
||||||
onTap: () => _openPathMap(context, initialPath: variants[i]),
|
onTap: () => _openPathMap(
|
||||||
|
context,
|
||||||
|
initialPath: variants[i],
|
||||||
|
channelMessage: channelMessage,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -248,13 +263,18 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _openPathMap(BuildContext context, {Uint8List? initialPath}) {
|
void _openPathMap(
|
||||||
|
BuildContext context, {
|
||||||
|
Uint8List? initialPath,
|
||||||
|
bool channelMessage = false,
|
||||||
|
}) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => ChannelMessagePathMapScreen(
|
builder: (context) => ChannelMessagePathMapScreen(
|
||||||
message: message,
|
message: message,
|
||||||
initialPath: initialPath,
|
initialPath: initialPath,
|
||||||
|
channelMessage: channelMessage,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -264,11 +284,13 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
class ChannelMessagePathMapScreen extends StatefulWidget {
|
class ChannelMessagePathMapScreen extends StatefulWidget {
|
||||||
final ChannelMessage message;
|
final ChannelMessage message;
|
||||||
final Uint8List? initialPath;
|
final Uint8List? initialPath;
|
||||||
|
final bool channelMessage;
|
||||||
|
|
||||||
const ChannelMessagePathMapScreen({
|
const ChannelMessagePathMapScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.message,
|
required this.message,
|
||||||
this.initialPath,
|
this.initialPath,
|
||||||
|
this.channelMessage = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -278,8 +300,12 @@ class ChannelMessagePathMapScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _ChannelMessagePathMapScreenState
|
class _ChannelMessagePathMapScreenState
|
||||||
extends State<ChannelMessagePathMapScreen> {
|
extends State<ChannelMessagePathMapScreen> {
|
||||||
|
static const double _labelZoomThreshold = 8.5;
|
||||||
|
|
||||||
Uint8List? _selectedPath;
|
Uint8List? _selectedPath;
|
||||||
double _pathDistance = 0.0;
|
double _pathDistance = 0.0;
|
||||||
|
bool _showNodeLabels = true;
|
||||||
|
bool _didReceivePositionUpdate = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -314,6 +340,8 @@ class _ChannelMessagePathMapScreenState
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<MeshCoreConnector>(
|
return Consumer<MeshCoreConnector>(
|
||||||
builder: (context, connector, _) {
|
builder: (context, connector, _) {
|
||||||
|
final settings = context.watch<AppSettingsService>().settings;
|
||||||
|
final isImperial = settings.unitSystem == UnitSystem.imperial;
|
||||||
final tileCache = context.read<MapTileCacheService>();
|
final tileCache = context.read<MapTileCacheService>();
|
||||||
final primaryPath = _selectPrimaryPath(
|
final primaryPath = _selectPrimaryPath(
|
||||||
widget.message.pathBytes,
|
widget.message.pathBytes,
|
||||||
@@ -323,11 +351,18 @@ class _ChannelMessagePathMapScreenState
|
|||||||
primaryPath,
|
primaryPath,
|
||||||
widget.message.pathVariants,
|
widget.message.pathVariants,
|
||||||
);
|
);
|
||||||
final selectedPath = _resolveSelectedPath(
|
final selectedPathTmp = _resolveSelectedPath(
|
||||||
_selectedPath,
|
_selectedPath,
|
||||||
observedPaths,
|
observedPaths,
|
||||||
primaryPath,
|
primaryPath,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final selectedPath =
|
||||||
|
((!widget.message.isOutgoing && !widget.channelMessage) ||
|
||||||
|
(widget.message.isOutgoing && widget.channelMessage))
|
||||||
|
? Uint8List.fromList(selectedPathTmp.reversed.toList())
|
||||||
|
: selectedPathTmp;
|
||||||
|
|
||||||
final selectedIndex = _indexForPath(selectedPath, observedPaths);
|
final selectedIndex = _indexForPath(selectedPath, observedPaths);
|
||||||
final hops = _buildPathHops(
|
final hops = _buildPathHops(
|
||||||
selectedPath,
|
selectedPath,
|
||||||
@@ -336,12 +371,22 @@ class _ChannelMessagePathMapScreenState
|
|||||||
);
|
);
|
||||||
|
|
||||||
final points = <LatLng>[];
|
final points = <LatLng>[];
|
||||||
|
|
||||||
|
if ((widget.message.isOutgoing && !widget.channelMessage) ||
|
||||||
|
(widget.message.isOutgoing && widget.channelMessage)) {
|
||||||
|
points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
||||||
|
}
|
||||||
|
|
||||||
for (final hop in hops) {
|
for (final hop in hops) {
|
||||||
if (hop.hasLocation) {
|
if (hop.hasLocation) {
|
||||||
points.add(hop.position!);
|
points.add(hop.position!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
|
||||||
|
if ((!widget.message.isOutgoing && !widget.channelMessage) ||
|
||||||
|
(!widget.message.isOutgoing && widget.channelMessage)) {
|
||||||
|
points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
||||||
|
}
|
||||||
|
|
||||||
final polylines = points.length > 1
|
final polylines = points.length > 1
|
||||||
? [
|
? [
|
||||||
@@ -357,6 +402,9 @@ class _ChannelMessagePathMapScreenState
|
|||||||
? points.first
|
? points.first
|
||||||
: const LatLng(0, 0);
|
: const LatLng(0, 0);
|
||||||
final initialZoom = points.isNotEmpty ? 13.0 : 2.0;
|
final initialZoom = points.isNotEmpty ? 13.0 : 2.0;
|
||||||
|
if (!_didReceivePositionUpdate) {
|
||||||
|
_showNodeLabels = initialZoom >= _labelZoomThreshold;
|
||||||
|
}
|
||||||
final bounds = points.length > 1
|
final bounds = points.length > 1
|
||||||
? LatLngBounds.fromPoints(points)
|
? LatLngBounds.fromPoints(points)
|
||||||
: null;
|
: null;
|
||||||
@@ -366,7 +414,9 @@ class _ChannelMessagePathMapScreenState
|
|||||||
_pathDistance = _getPathDistance(points);
|
_pathDistance = _getPathDistance(points);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: Text(context.l10n.channelPath_mapTitle)),
|
appBar: AppBar(
|
||||||
|
title: AdaptiveAppBarTitle(context.l10n.channelPath_mapTitle),
|
||||||
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
@@ -388,6 +438,17 @@ class _ChannelMessagePathMapScreenState
|
|||||||
interactionOptions: InteractionOptions(
|
interactionOptions: InteractionOptions(
|
||||||
flags: ~InteractiveFlag.rotate,
|
flags: ~InteractiveFlag.rotate,
|
||||||
),
|
),
|
||||||
|
onPositionChanged: (camera, hasGesture) {
|
||||||
|
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
||||||
|
if (!_didReceivePositionUpdate ||
|
||||||
|
shouldShow != _showNodeLabels) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_didReceivePositionUpdate = true;
|
||||||
|
_showNodeLabels = shouldShow;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
TileLayer(
|
TileLayer(
|
||||||
@@ -399,7 +460,12 @@ class _ChannelMessagePathMapScreenState
|
|||||||
),
|
),
|
||||||
if (polylines.isNotEmpty)
|
if (polylines.isNotEmpty)
|
||||||
PolylineLayer(polylines: polylines),
|
PolylineLayer(polylines: polylines),
|
||||||
MarkerLayer(markers: _buildHopMarkers(hops)),
|
MarkerLayer(
|
||||||
|
markers: _buildHopMarkers(
|
||||||
|
hops,
|
||||||
|
showLabels: _showNodeLabels,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (observedPaths.length > 1)
|
if (observedPaths.length > 1)
|
||||||
@@ -422,7 +488,7 @@ class _ChannelMessagePathMapScreenState
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildLegendCard(context, hops),
|
_buildLegendCard(context, hops, isImperial),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -494,45 +560,61 @@ class _ChannelMessagePathMapScreenState
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Marker> _buildHopMarkers(List<_PathHop> hops) {
|
List<Marker> _buildHopMarkers(
|
||||||
return [
|
List<_PathHop> hops, {
|
||||||
for (final hop in hops)
|
required bool showLabels,
|
||||||
if (hop.hasLocation)
|
}) {
|
||||||
Marker(
|
final markers = <Marker>[];
|
||||||
point: hop.position!,
|
for (final hop in hops) {
|
||||||
width: 35,
|
if (!hop.hasLocation) continue;
|
||||||
height: 35,
|
final point = hop.position!;
|
||||||
child: Container(
|
markers.add(
|
||||||
decoration: BoxDecoration(
|
Marker(
|
||||||
color: Colors.green,
|
point: point,
|
||||||
shape: BoxShape.circle,
|
width: 35,
|
||||||
border: Border.all(color: Colors.white, width: 2),
|
height: 35,
|
||||||
boxShadow: [
|
child: Container(
|
||||||
BoxShadow(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black.withValues(alpha: 0.3),
|
color: Colors.green,
|
||||||
blurRadius: 4,
|
shape: BoxShape.circle,
|
||||||
offset: const Offset(0, 2),
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
),
|
boxShadow: [
|
||||||
],
|
BoxShadow(
|
||||||
),
|
color: Colors.black.withValues(alpha: 0.3),
|
||||||
alignment: Alignment.center,
|
blurRadius: 4,
|
||||||
child: Text(
|
offset: const Offset(0, 2),
|
||||||
hop.index.toString(),
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
hop.index.toString(),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (context.read<MeshCoreConnector>().selfLatitude != null &&
|
),
|
||||||
context.read<MeshCoreConnector>().selfLongitude != null)
|
);
|
||||||
Marker(
|
if (showLabels) {
|
||||||
point: LatLng(
|
markers.add(
|
||||||
context.read<MeshCoreConnector>().selfLatitude!,
|
_buildNodeLabelMarker(
|
||||||
context.read<MeshCoreConnector>().selfLongitude!,
|
point: point,
|
||||||
|
label: hop.contact?.name ?? _formatPrefix(hop.prefix),
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final selfLat = context.read<MeshCoreConnector>().selfLatitude;
|
||||||
|
final selfLon = context.read<MeshCoreConnector>().selfLongitude;
|
||||||
|
if (selfLat != null && selfLon != null) {
|
||||||
|
final selfPoint = LatLng(selfLat, selfLon);
|
||||||
|
markers.add(
|
||||||
|
Marker(
|
||||||
|
point: selfPoint,
|
||||||
width: 35,
|
width: 35,
|
||||||
height: 35,
|
height: 35,
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -559,10 +641,60 @@ class _ChannelMessagePathMapScreenState
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
);
|
||||||
|
if (showLabels) {
|
||||||
|
markers.add(
|
||||||
|
_buildNodeLabelMarker(
|
||||||
|
point: selfPoint,
|
||||||
|
label: context.l10n.pathTrace_you,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return markers;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLegendCard(BuildContext context, List<_PathHop> hops) {
|
Marker _buildNodeLabelMarker({required LatLng point, required String label}) {
|
||||||
|
return Marker(
|
||||||
|
point: point,
|
||||||
|
width: 120,
|
||||||
|
height: 24,
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: Transform.translate(
|
||||||
|
offset: const Offset(0, -20),
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black54,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLegendCard(
|
||||||
|
BuildContext context,
|
||||||
|
List<_PathHop> hops,
|
||||||
|
bool isImperial,
|
||||||
|
) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
final maxHeight = MediaQuery.of(context).size.height * 0.35;
|
final maxHeight = MediaQuery.of(context).size.height * 0.35;
|
||||||
final estimatedHeight = 72.0 + (hops.length * 56.0);
|
final estimatedHeight = 72.0 + (hops.length * 56.0);
|
||||||
@@ -581,7 +713,7 @@ class _ChannelMessagePathMapScreenState
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
child: Text(
|
child: Text(
|
||||||
'${l10n.channelPath_repeaterHops} (${(_pathDistance / 1609.34).toStringAsFixed(2)} Miles / ${(_pathDistance / 1000).toStringAsFixed(2)} Km)',
|
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistance, isImperial: isImperial)}',
|
||||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,18 +3,20 @@ import 'dart:math';
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:meshcore_open/storage/channel_message_store.dart';
|
||||||
|
import 'package:meshcore_open/widgets/app_bar.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
|
import '../services/app_settings_service.dart';
|
||||||
import '../models/channel.dart';
|
import '../models/channel.dart';
|
||||||
import '../models/community.dart';
|
import '../models/community.dart';
|
||||||
import '../storage/community_store.dart';
|
import '../storage/community_store.dart';
|
||||||
import '../utils/dialog_utils.dart';
|
import '../utils/dialog_utils.dart';
|
||||||
import '../utils/disconnect_navigation_mixin.dart';
|
import '../utils/disconnect_navigation_mixin.dart';
|
||||||
import '../utils/route_transitions.dart';
|
import '../utils/route_transitions.dart';
|
||||||
import '../widgets/battery_indicator.dart';
|
|
||||||
import '../widgets/list_filter_widget.dart';
|
import '../widgets/list_filter_widget.dart';
|
||||||
import '../widgets/empty_state.dart';
|
import '../widgets/empty_state.dart';
|
||||||
import '../widgets/qr_code_display.dart';
|
import '../widgets/qr_code_display.dart';
|
||||||
@@ -104,6 +106,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final connector = context.watch<MeshCoreConnector>();
|
final connector = context.watch<MeshCoreConnector>();
|
||||||
|
final channelMessageStore = ChannelMessageStore();
|
||||||
|
|
||||||
// Auto-navigate back to scanner if disconnected
|
// Auto-navigate back to scanner if disconnected
|
||||||
if (!checkConnectionAndNavigate(connector)) {
|
if (!checkConnectionAndNavigate(connector)) {
|
||||||
@@ -116,8 +119,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
canPop: allowBack,
|
canPop: allowBack,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: BatteryIndicator(connector: connector),
|
title: AppBarTitle(context.l10n.channels_title),
|
||||||
title: Text(context.l10n.channels_title),
|
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
actions: [
|
actions: [
|
||||||
@@ -304,6 +306,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
return _buildChannelTile(
|
return _buildChannelTile(
|
||||||
context,
|
context,
|
||||||
connector,
|
connector,
|
||||||
|
channelMessageStore,
|
||||||
channel,
|
channel,
|
||||||
showDragHandle: true,
|
showDragHandle: true,
|
||||||
dragIndex: index,
|
dragIndex: index,
|
||||||
@@ -323,6 +326,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
return _buildChannelTile(
|
return _buildChannelTile(
|
||||||
context,
|
context,
|
||||||
connector,
|
connector,
|
||||||
|
channelMessageStore,
|
||||||
channel,
|
channel,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -352,6 +356,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
Widget _buildChannelTile(
|
Widget _buildChannelTile(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
MeshCoreConnector connector,
|
MeshCoreConnector connector,
|
||||||
|
ChannelMessageStore channelMessageStore,
|
||||||
Channel channel, {
|
Channel channel, {
|
||||||
bool showDragHandle = false,
|
bool showDragHandle = false,
|
||||||
int? dragIndex,
|
int? dragIndex,
|
||||||
@@ -468,7 +473,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongPress: () => _showChannelActions(context, connector, channel),
|
onLongPress: () => _showChannelActions(
|
||||||
|
context,
|
||||||
|
connector,
|
||||||
|
channelMessageStore,
|
||||||
|
channel,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -476,11 +486,16 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
void _showChannelActions(
|
void _showChannelActions(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
MeshCoreConnector connector,
|
MeshCoreConnector connector,
|
||||||
|
ChannelMessageStore channelMessageStore,
|
||||||
Channel channel,
|
Channel channel,
|
||||||
) {
|
) {
|
||||||
|
final parentContext = context;
|
||||||
|
final settingsService = context.read<AppSettingsService>();
|
||||||
|
final isMuted = settingsService.isChannelMuted(channel.name);
|
||||||
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: parentContext,
|
||||||
builder: (context) => SafeArea(
|
builder: (sheetContext) => SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -488,10 +503,30 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
leading: const Icon(Icons.edit_outlined),
|
leading: const Icon(Icons.edit_outlined),
|
||||||
title: Text(context.l10n.channels_editChannel),
|
title: Text(context.l10n.channels_editChannel),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
Navigator.pop(context);
|
Navigator.pop(sheetContext);
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
if (context.mounted) {
|
if (parentContext.mounted) {
|
||||||
_showEditChannelDialog(context, connector, channel);
|
_showEditChannelDialog(parentContext, connector, channel);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
isMuted
|
||||||
|
? Icons.notifications_outlined
|
||||||
|
: Icons.notifications_off_outlined,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
isMuted
|
||||||
|
? context.l10n.channels_unmuteChannel
|
||||||
|
: context.l10n.channels_muteChannel,
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
Navigator.pop(sheetContext);
|
||||||
|
if (isMuted) {
|
||||||
|
await settingsService.unmuteChannel(channel.name);
|
||||||
|
} else {
|
||||||
|
await settingsService.muteChannel(channel.name);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -502,10 +537,15 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
style: const TextStyle(color: Colors.red),
|
style: const TextStyle(color: Colors.red),
|
||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
Navigator.pop(context);
|
Navigator.pop(sheetContext);
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
if (context.mounted) {
|
if (parentContext.mounted) {
|
||||||
_confirmDeleteChannel(context, connector, channel);
|
_confirmDeleteChannel(
|
||||||
|
context,
|
||||||
|
connector,
|
||||||
|
channelMessageStore,
|
||||||
|
channel,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -1415,7 +1455,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
child: Text(dialogContext.l10n.common_cancel),
|
child: Text(dialogContext.l10n.common_cancel),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
final name = nameController.text.trim();
|
final name = nameController.text.trim();
|
||||||
final pskHex = pskController.text.trim();
|
final pskHex = pskController.text.trim();
|
||||||
|
|
||||||
@@ -1432,13 +1472,25 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Navigator.pop(dialogContext);
|
Navigator.pop(dialogContext);
|
||||||
connector.setChannel(channel.index, name, psk);
|
try {
|
||||||
connector.setChannelSmazEnabled(channel.index, smazEnabled);
|
await connector.setChannel(channel.index, name, psk);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
await connector.setChannelSmazEnabled(
|
||||||
SnackBar(
|
channel.index,
|
||||||
content: Text(context.l10n.channels_channelUpdated(name)),
|
smazEnabled,
|
||||||
),
|
);
|
||||||
);
|
if (!context.mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(context.l10n.channels_channelUpdated(name)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e, st) {
|
||||||
|
debugPrint(st.toString());
|
||||||
|
if (!context.mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Failed to update channel: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: Text(dialogContext.l10n.common_save),
|
child: Text(dialogContext.l10n.common_save),
|
||||||
),
|
),
|
||||||
@@ -1451,6 +1503,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
void _confirmDeleteChannel(
|
void _confirmDeleteChannel(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
MeshCoreConnector connector,
|
MeshCoreConnector connector,
|
||||||
|
ChannelMessageStore channelMessageStore,
|
||||||
Channel channel,
|
Channel channel,
|
||||||
) {
|
) {
|
||||||
showDialog(
|
showDialog(
|
||||||
@@ -1466,16 +1519,36 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
child: Text(dialogContext.l10n.common_cancel),
|
child: Text(dialogContext.l10n.common_cancel),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
Navigator.pop(dialogContext);
|
Navigator.pop(dialogContext);
|
||||||
connector.deleteChannel(channel.index);
|
try {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
await connector.deleteChannel(channel.index);
|
||||||
SnackBar(
|
|
||||||
content: Text(
|
channelMessageStore.clearChannelMessages(channel.index);
|
||||||
context.l10n.channels_channelDeleted(channel.name),
|
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
context.l10n.channels_channelDeleted(channel.name),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
} catch (e, st) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
context.l10n.channels_channelDeleteFailed(channel.name),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Preserve existing logging (if it was there)
|
||||||
|
debugPrint('Failed to delete channel: $e\n$st');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
dialogContext.l10n.common_delete,
|
dialogContext.l10n.common_delete,
|
||||||
|
|||||||
+571
-372
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,89 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../l10n/l10n.dart';
|
||||||
|
|
||||||
|
class ChromeRequiredScreen extends StatelessWidget {
|
||||||
|
const ChromeRequiredScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final isDark = theme.brightness == Brightness.dark;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: isDark
|
||||||
|
? [const Color(0xFF1A1A1A), const Color(0xFF0D0D0D)]
|
||||||
|
: [const Color(0xFFF5F7FA), const Color(0xFFE4E7EB)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.orange.withValues(alpha: 0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.browser_not_supported_rounded,
|
||||||
|
size: 80,
|
||||||
|
color: Colors.orange,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
Text(
|
||||||
|
l10n.scanner_chromeRequired,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: theme.textTheme.headlineMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: isDark ? Colors.white : Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
l10n.scanner_chromeRequiredMessage,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: isDark ? Colors.white70 : Colors.black54,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 48),
|
||||||
|
// We can't really "fix" it for them other than telling them to use Chrome
|
||||||
|
// but we can provide a nice visual.
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue.withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(30),
|
||||||
|
border: Border.all(color: Colors.blue.withValues(alpha: 0.3)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.info_outline, size: 20, color: Colors.blue),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
"Web Bluetooth requires a Chromium browser",
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Colors.blue,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import '../connector/meshcore_connector.dart';
|
|||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../models/community.dart';
|
import '../models/community.dart';
|
||||||
import '../storage/community_store.dart';
|
import '../storage/community_store.dart';
|
||||||
|
import '../widgets/adaptive_app_bar_title.dart';
|
||||||
import '../widgets/qr_scanner_widget.dart';
|
import '../widgets/qr_scanner_widget.dart';
|
||||||
|
|
||||||
/// Screen for scanning community QR codes to join communities.
|
/// Screen for scanning community QR codes to join communities.
|
||||||
@@ -29,7 +30,7 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(context.l10n.community_scanQr),
|
title: AdaptiveAppBarTitle(context.l10n.community_scanQr),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
),
|
),
|
||||||
body: _isProcessing
|
body: _isProcessing
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import 'dart:async';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:meshcore_open/screens/path_trace_map.dart';
|
import 'package:meshcore_open/screens/path_trace_map.dart';
|
||||||
|
import 'package:meshcore_open/utils/app_logger.dart';
|
||||||
|
import 'package:meshcore_open/widgets/app_bar.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
@@ -16,7 +18,6 @@ import '../utils/dialog_utils.dart';
|
|||||||
import '../utils/disconnect_navigation_mixin.dart';
|
import '../utils/disconnect_navigation_mixin.dart';
|
||||||
import '../utils/emoji_utils.dart';
|
import '../utils/emoji_utils.dart';
|
||||||
import '../utils/route_transitions.dart';
|
import '../utils/route_transitions.dart';
|
||||||
import '../widgets/battery_indicator.dart';
|
|
||||||
import '../widgets/list_filter_widget.dart';
|
import '../widgets/list_filter_widget.dart';
|
||||||
import '../widgets/empty_state.dart';
|
import '../widgets/empty_state.dart';
|
||||||
import '../widgets/quick_switch_bar.dart';
|
import '../widgets/quick_switch_bar.dart';
|
||||||
@@ -25,6 +26,7 @@ import '../widgets/room_login_dialog.dart';
|
|||||||
import '../widgets/unread_badge.dart';
|
import '../widgets/unread_badge.dart';
|
||||||
import 'channels_screen.dart';
|
import 'channels_screen.dart';
|
||||||
import 'chat_screen.dart';
|
import 'chat_screen.dart';
|
||||||
|
import 'discovery_screen.dart';
|
||||||
import 'map_screen.dart';
|
import 'map_screen.dart';
|
||||||
import 'repeater_hub_screen.dart';
|
import 'repeater_hub_screen.dart';
|
||||||
import 'settings_screen.dart';
|
import 'settings_screen.dart';
|
||||||
@@ -90,79 +92,90 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
_frameSubscription = connector.receivedFrames.listen((frame) {
|
_frameSubscription = connector.receivedFrames.listen((frame) {
|
||||||
if (frame.isEmpty) return;
|
if (frame.isEmpty) return;
|
||||||
final frameBuffer = BufferReader(frame);
|
final frameBuffer = BufferReader(frame);
|
||||||
final code = frameBuffer.readUInt8();
|
try {
|
||||||
|
final code = frameBuffer.readUInt8();
|
||||||
|
|
||||||
if (code == respCodeExportContact) {
|
if (code == respCodeExportContact) {
|
||||||
final advertPacket = frameBuffer.readRemainingBytes();
|
final advertPacket = frameBuffer.readRemainingBytes();
|
||||||
// Validate packet has expected minimum size (98+ bytes per protocol)
|
// Validate packet has expected minimum size (98+ bytes per protocol)
|
||||||
if (advertPacket.length < 98) {
|
if (advertPacket.length < 98) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(context.l10n.contacts_invalidAdvertFormat),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_pendingOperations.remove(ContactOperationType.export);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final hexString = pubKeyToHex(advertPacket);
|
||||||
|
Clipboard.setData(ClipboardData(text: "meshcore://$hexString"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code == respCodeOk) {
|
||||||
|
// Show a snackbar indicating success
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (_pendingOperations.contains(ContactOperationType.import)) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(context.l10n.contacts_contactImported)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(context.l10n.contacts_invalidAdvertFormat),
|
content: Text(context.l10n.contacts_zeroHopContactAdvertSent),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_pendingOperations.remove(ContactOperationType.export);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final hexString = pubKeyToHex(advertPacket);
|
|
||||||
Clipboard.setData(ClipboardData(text: "meshcore://$hexString"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (code == respCodeOk) {
|
if (_pendingOperations.contains(ContactOperationType.export)) {
|
||||||
// Show a snackbar indicating success
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
if (!mounted) return;
|
SnackBar(
|
||||||
|
content: Text(context.l10n.contacts_contactAdvertCopied),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (_pendingOperations.contains(ContactOperationType.import)) {
|
_pendingOperations.clear();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text(context.l10n.contacts_contactImported)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
|
if (code == respCodeErr) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
// Show a snackbar indicating failure
|
||||||
SnackBar(
|
if (!mounted) return;
|
||||||
content: Text(context.l10n.contacts_zeroHopContactAdvertSent),
|
|
||||||
),
|
if (_pendingOperations.contains(ContactOperationType.import)) {
|
||||||
);
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(context.l10n.contacts_contactImportFailed),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(context.l10n.contacts_zeroHopContactAdvertFailed),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (_pendingOperations.contains(ContactOperationType.export)) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(context.l10n.contacts_contactAdvertCopyFailed),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_pendingOperations.clear();
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
if (_pendingOperations.contains(ContactOperationType.export)) {
|
appLogger.error(
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
'Error processing received frame: $e',
|
||||||
SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)),
|
tag: 'ContactsScreen',
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
_pendingOperations.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (code == respCodeErr) {
|
|
||||||
// Show a snackbar indicating failure
|
|
||||||
if (!mounted) return;
|
|
||||||
|
|
||||||
if (_pendingOperations.contains(ContactOperationType.import)) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text(context.l10n.contacts_contactImportFailed)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(context.l10n.contacts_zeroHopContactAdvertFailed),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (_pendingOperations.contains(ContactOperationType.export)) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(context.l10n.contacts_contactAdvertCopyFailed),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_pendingOperations.clear();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -171,14 +184,17 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
final exportContactFrame = buildExportContactFrame(pubKey);
|
final exportContactFrame = buildExportContactFrame(pubKey);
|
||||||
_pendingOperations.add(ContactOperationType.export);
|
_pendingOperations.add(ContactOperationType.export);
|
||||||
await connector.sendFrame(exportContactFrame);
|
await connector.sendFrame(exportContactFrame, expectsGenericAck: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _contactZeroHop(Uint8List pubKey) async {
|
Future<void> _contactZeroHop(Uint8List pubKey) async {
|
||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
final exportContactZeroHopFrame = buildZeroHopContact(pubKey);
|
final exportContactZeroHopFrame = buildZeroHopContact(pubKey);
|
||||||
_pendingOperations.add(ContactOperationType.zeroHopShare);
|
_pendingOperations.add(ContactOperationType.zeroHopShare);
|
||||||
await connector.sendFrame(exportContactZeroHopFrame);
|
await connector.sendFrame(
|
||||||
|
exportContactZeroHopFrame,
|
||||||
|
expectsGenericAck: true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _contactImport() async {
|
Future<void> _contactImport() async {
|
||||||
@@ -203,9 +219,10 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
}
|
}
|
||||||
final hexString = text.substring('meshcore://'.length);
|
final hexString = text.substring('meshcore://'.length);
|
||||||
try {
|
try {
|
||||||
final importContactFrame = buildImportContactFrame(hexString);
|
final bytes = hex2Uint8List(hexString);
|
||||||
|
final importContactFrame = buildImportContactFrame(bytes);
|
||||||
_pendingOperations.add(ContactOperationType.import);
|
_pendingOperations.add(ContactOperationType.import);
|
||||||
await connector.sendFrame(importContactFrame);
|
connector.importContact(importContactFrame);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -229,9 +246,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
canPop: allowBack,
|
canPop: allowBack,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: BatteryIndicator(connector: connector),
|
title: AppBarTitle(context.l10n.contacts_title),
|
||||||
title: Text(context.l10n.contacts_title),
|
|
||||||
centerTitle: true,
|
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
actions: [
|
actions: [
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
@@ -305,6 +320,21 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
),
|
),
|
||||||
onTap: () => _disconnect(context, connector),
|
onTap: () => _disconnect(context, connector),
|
||||||
),
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.person_add_rounded),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text("Discovered Contacts"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () => Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const DiscoveryScreen(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -371,8 +401,15 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
|
|
||||||
Widget _buildContactsBody(BuildContext context, MeshCoreConnector connector) {
|
Widget _buildContactsBody(BuildContext context, MeshCoreConnector connector) {
|
||||||
final contacts = connector.contacts;
|
final contacts = connector.contacts;
|
||||||
|
final shouldShowStartupSpinner =
|
||||||
|
contacts.isEmpty &&
|
||||||
|
_groups.isEmpty &&
|
||||||
|
connector.isConnected &&
|
||||||
|
(connector.isLoadingContacts ||
|
||||||
|
connector.isLoadingChannels ||
|
||||||
|
connector.selfPublicKey == null);
|
||||||
|
|
||||||
if (contacts.isEmpty && connector.isLoadingContacts && _groups.isEmpty) {
|
if (shouldShowStartupSpinner) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,6 +426,41 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
? const <ContactGroup>[]
|
? const <ContactGroup>[]
|
||||||
: _filterAndSortGroups(_groups, contacts);
|
: _filterAndSortGroups(_groups, contacts);
|
||||||
|
|
||||||
|
String hintText = "";
|
||||||
|
|
||||||
|
switch (_typeFilter) {
|
||||||
|
case ContactTypeFilter.all:
|
||||||
|
hintText = context.l10n.contacts_searchContacts(
|
||||||
|
filteredAndSorted.length,
|
||||||
|
_showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ContactTypeFilter.users:
|
||||||
|
hintText = context.l10n.contacts_searchUsers(
|
||||||
|
filteredAndSorted.length,
|
||||||
|
_showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ContactTypeFilter.repeaters:
|
||||||
|
hintText = context.l10n.contacts_searchRepeaters(
|
||||||
|
filteredAndSorted.length,
|
||||||
|
_showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ContactTypeFilter.rooms:
|
||||||
|
hintText = context.l10n.contacts_searchRoomServers(
|
||||||
|
filteredAndSorted.length,
|
||||||
|
_showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ContactTypeFilter.favorites:
|
||||||
|
hintText = context.l10n.contacts_searchFavorites(
|
||||||
|
filteredAndSorted.length,
|
||||||
|
_showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
@@ -396,7 +468,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _searchController,
|
controller: _searchController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: context.l10n.contacts_searchContacts,
|
hintText: hintText,
|
||||||
prefixIcon: const Icon(Icons.search),
|
prefixIcon: const Icon(Icons.search),
|
||||||
suffixIcon: Row(
|
suffixIcon: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -468,6 +540,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
contact: contact,
|
contact: contact,
|
||||||
lastSeen: _resolveLastSeen(contact),
|
lastSeen: _resolveLastSeen(contact),
|
||||||
unreadCount: unreadCount,
|
unreadCount: unreadCount,
|
||||||
|
isFavorite: contact.isFavorite,
|
||||||
onTap: () => _openChat(context, contact),
|
onTap: () => _openChat(context, contact),
|
||||||
onLongPress: () =>
|
onLongPress: () =>
|
||||||
_showContactOptions(context, connector, contact),
|
_showContactOptions(context, connector, contact),
|
||||||
@@ -504,6 +577,8 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
})
|
})
|
||||||
.where((group) {
|
.where((group) {
|
||||||
if (_typeFilter == ContactTypeFilter.all) return true;
|
if (_typeFilter == ContactTypeFilter.all) return true;
|
||||||
|
// Groups don't have a favorite flag, so hide them under favorites filter
|
||||||
|
if (_typeFilter == ContactTypeFilter.favorites) return false;
|
||||||
for (final key in group.memberKeys) {
|
for (final key in group.memberKeys) {
|
||||||
final contact = contactsByKey[key];
|
final contact = contactsByKey[key];
|
||||||
if (contact != null && _matchesTypeFilter(contact)) return true;
|
if (contact != null && _matchesTypeFilter(contact)) return true;
|
||||||
@@ -578,6 +653,8 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
switch (_typeFilter) {
|
switch (_typeFilter) {
|
||||||
case ContactTypeFilter.all:
|
case ContactTypeFilter.all:
|
||||||
return true;
|
return true;
|
||||||
|
case ContactTypeFilter.favorites:
|
||||||
|
return contact.isFavorite;
|
||||||
case ContactTypeFilter.users:
|
case ContactTypeFilter.users:
|
||||||
return contact.type == advTypeChat;
|
return contact.type == advTypeChat;
|
||||||
case ContactTypeFilter.repeaters:
|
case ContactTypeFilter.repeaters:
|
||||||
@@ -968,6 +1045,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
) {
|
) {
|
||||||
final isRepeater = contact.type == advTypeRepeater;
|
final isRepeater = contact.type == advTypeRepeater;
|
||||||
final isRoom = contact.type == advTypeRoom;
|
final isRoom = contact.type == advTypeRoom;
|
||||||
|
final isFavorite = contact.isFavorite;
|
||||||
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -1060,6 +1138,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
contact.name,
|
contact.name,
|
||||||
),
|
),
|
||||||
path: contact.traceRouteBytes ?? Uint8List(0),
|
path: contact.traceRouteBytes ?? Uint8List(0),
|
||||||
|
targetContact: contact,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1074,6 +1153,21 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
isFavorite ? Icons.star : Icons.star_border,
|
||||||
|
color: Colors.amber[700],
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
isFavorite
|
||||||
|
? context.l10n.listFilter_removeFromFavorites
|
||||||
|
: context.l10n.listFilter_addToFavorites,
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
Navigator.pop(sheetContext);
|
||||||
|
await connector.setContactFavorite(contact, !isFavorite);
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.copy),
|
leading: const Icon(Icons.copy),
|
||||||
title: Text(context.l10n.contacts_ShareContact),
|
title: Text(context.l10n.contacts_ShareContact),
|
||||||
@@ -1142,6 +1236,7 @@ class _ContactTile extends StatelessWidget {
|
|||||||
final Contact contact;
|
final Contact contact;
|
||||||
final DateTime lastSeen;
|
final DateTime lastSeen;
|
||||||
final int unreadCount;
|
final int unreadCount;
|
||||||
|
final bool isFavorite;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
final VoidCallback onLongPress;
|
final VoidCallback onLongPress;
|
||||||
|
|
||||||
@@ -1149,6 +1244,7 @@ class _ContactTile extends StatelessWidget {
|
|||||||
required this.contact,
|
required this.contact,
|
||||||
required this.lastSeen,
|
required this.lastSeen,
|
||||||
required this.unreadCount,
|
required this.unreadCount,
|
||||||
|
required this.isFavorite,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
required this.onLongPress,
|
required this.onLongPress,
|
||||||
});
|
});
|
||||||
@@ -1160,12 +1256,17 @@ class _ContactTile extends StatelessWidget {
|
|||||||
backgroundColor: _getTypeColor(contact.type),
|
backgroundColor: _getTypeColor(contact.type),
|
||||||
child: _buildContactAvatar(contact),
|
child: _buildContactAvatar(contact),
|
||||||
),
|
),
|
||||||
title: Text(contact.name),
|
title: Text(contact.name, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||||
subtitle: Column(
|
subtitle: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(contact.pathLabel),
|
Text(contact.pathLabel, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||||
Text(contact.shortPubKeyHex, style: TextStyle(fontSize: 12)),
|
Text(
|
||||||
|
contact.shortPubKeyHex,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
// Clamp text scaling in trailing section to prevent overflow while
|
// Clamp text scaling in trailing section to prevent overflow while
|
||||||
@@ -1176,26 +1277,36 @@ class _ContactTile extends StatelessWidget {
|
|||||||
MediaQuery.textScalerOf(context).scale(1.0).clamp(1.0, 1.3),
|
MediaQuery.textScalerOf(context).scale(1.0).clamp(1.0, 1.3),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: SizedBox(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
width: 120,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
child: Column(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
if (unreadCount > 0) ...[
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
UnreadBadge(count: unreadCount),
|
children: [
|
||||||
const SizedBox(height: 4),
|
if (unreadCount > 0) ...[
|
||||||
],
|
UnreadBadge(count: unreadCount),
|
||||||
Text(
|
const SizedBox(height: 4),
|
||||||
_formatLastSeen(context, lastSeen),
|
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
if (contact.hasLocation)
|
|
||||||
Icon(Icons.location_on, size: 14, color: Colors.grey[400]),
|
|
||||||
],
|
],
|
||||||
),
|
Text(
|
||||||
],
|
_formatLastSeen(context, lastSeen),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (isFavorite)
|
||||||
|
Icon(Icons.star, size: 14, color: Colors.amber[700]),
|
||||||
|
if (isFavorite && contact.hasLocation)
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
if (contact.hasLocation)
|
||||||
|
Icon(Icons.location_on, size: 14, color: Colors.grey[400]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
|
|||||||
@@ -0,0 +1,419 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../connector/meshcore_connector.dart';
|
||||||
|
import '../connector/meshcore_protocol.dart';
|
||||||
|
import '../l10n/l10n.dart';
|
||||||
|
import '../models/discovery_contact.dart';
|
||||||
|
import '../utils/contact_search.dart';
|
||||||
|
import '../widgets/app_bar.dart';
|
||||||
|
import '../widgets/list_filter_widget.dart';
|
||||||
|
|
||||||
|
enum DiscoverySortOption { lastSeen, name, type }
|
||||||
|
|
||||||
|
class DiscoveryScreen extends StatefulWidget {
|
||||||
|
const DiscoveryScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DiscoveryScreen> createState() => _DiscoveryScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||||
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
String searchQuery = '';
|
||||||
|
ContactSortOption sortOption = ContactSortOption.lastSeen;
|
||||||
|
bool showUnreadOnly = false;
|
||||||
|
ContactTypeFilter typeFilter = ContactTypeFilter.all;
|
||||||
|
DiscoverySortOption discoverySortOption = DiscoverySortOption.lastSeen;
|
||||||
|
Timer? _searchDebounce;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_searchController.dispose();
|
||||||
|
_searchDebounce?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final connector = context.watch<MeshCoreConnector>();
|
||||||
|
|
||||||
|
final discoveredContacts = connector.discoveredContacts;
|
||||||
|
final filteredAndSorted = _filterAndSortContacts(
|
||||||
|
discoveredContacts,
|
||||||
|
connector,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: AppBarTitle(
|
||||||
|
l10n.discoveredContacts_Title,
|
||||||
|
indicators: false,
|
||||||
|
subtitle: false,
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
actions: [
|
||||||
|
PopupMenuButton(
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.delete, color: Colors.red),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(context.l10n.discoveredContacts_deleteContactAll),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_deleteContacts(context, connector);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
icon: const Icon(Icons.more_vert),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
_buildFilters(filteredAndSorted, connector),
|
||||||
|
Expanded(
|
||||||
|
child: discoveredContacts.isEmpty
|
||||||
|
? Center(child: Text(l10n.contacts_noContacts))
|
||||||
|
: filteredAndSorted.isEmpty
|
||||||
|
? Center(child: Text(l10n.discoveredContacts_noMatching))
|
||||||
|
: ListView.builder(
|
||||||
|
itemCount: filteredAndSorted.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final contact = filteredAndSorted[index];
|
||||||
|
return ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: _getTypeColor(contact.type),
|
||||||
|
child: Icon(
|
||||||
|
_getTypeIcon(contact.type),
|
||||||
|
color: Colors.white,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
contact.name,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
contact.shortPubKeyHex,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
trailing: Text(
|
||||||
|
_formatLastSeen(context, contact.lastSeen),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
connector.importDiscoveredContact(contact);
|
||||||
|
},
|
||||||
|
onLongPress: () =>
|
||||||
|
_showContactContextMenu(contact, connector),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showContactContextMenu(
|
||||||
|
DiscoveryContact contact,
|
||||||
|
MeshCoreConnector connector,
|
||||||
|
) async {
|
||||||
|
final action = await showModalBottomSheet<String>(
|
||||||
|
context: context,
|
||||||
|
showDragHandle: true,
|
||||||
|
builder: (sheetContext) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.add_reaction_sharp),
|
||||||
|
title: Text(l10n.discoveredContacts_addContact),
|
||||||
|
onTap: () => Navigator.of(sheetContext).pop('import_contact'),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.copy),
|
||||||
|
title: Text(l10n.discoveredContacts_copyContact),
|
||||||
|
onTap: () => Navigator.of(sheetContext).pop('copy_contact'),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.delete),
|
||||||
|
title: Text(l10n.discoveredContacts_deleteContact),
|
||||||
|
onTap: () => Navigator.of(sheetContext).pop('delete_contact'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mounted || action == null) return;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'import_contact':
|
||||||
|
connector.importDiscoveredContact(contact);
|
||||||
|
break;
|
||||||
|
case 'copy_contact':
|
||||||
|
final hexString = pubKeyToHex(contact.rawPacket);
|
||||||
|
Clipboard.setData(ClipboardData(text: "meshcore://$hexString"));
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'delete_contact':
|
||||||
|
connector.removeDiscoveredContact(contact);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _deleteContacts(BuildContext context, MeshCoreConnector connector) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text(l10n.common_deleteAll),
|
||||||
|
content: Text(l10n.discoveredContacts_deleteContactAllContent),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(l10n.common_cancel),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.pop(context);
|
||||||
|
connector.removeAllDiscoveredContacts();
|
||||||
|
},
|
||||||
|
child: Text(l10n.common_deleteAll),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFilters(
|
||||||
|
List<DiscoveryContact> filteredAndSorted,
|
||||||
|
MeshCoreConnector connector,
|
||||||
|
) {
|
||||||
|
String hintText = "";
|
||||||
|
switch (typeFilter) {
|
||||||
|
case ContactTypeFilter.all:
|
||||||
|
hintText = context.l10n.contacts_searchContacts(
|
||||||
|
filteredAndSorted.length,
|
||||||
|
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ContactTypeFilter.users:
|
||||||
|
hintText = context.l10n.contacts_searchUsers(
|
||||||
|
filteredAndSorted.length,
|
||||||
|
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ContactTypeFilter.repeaters:
|
||||||
|
hintText = context.l10n.contacts_searchRepeaters(
|
||||||
|
filteredAndSorted.length,
|
||||||
|
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ContactTypeFilter.rooms:
|
||||||
|
hintText = context.l10n.contacts_searchRoomServers(
|
||||||
|
filteredAndSorted.length,
|
||||||
|
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ContactTypeFilter.favorites:
|
||||||
|
hintText = context.l10n.contacts_searchFavorites(
|
||||||
|
filteredAndSorted.length,
|
||||||
|
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: TextField(
|
||||||
|
controller: _searchController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: hintText,
|
||||||
|
prefixIcon: const Icon(Icons.search),
|
||||||
|
suffixIcon: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (searchQuery.isNotEmpty)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
onPressed: () {
|
||||||
|
_searchController.clear();
|
||||||
|
setState(() {
|
||||||
|
searchQuery = '';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_buildFilterButton(context, connector),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
_searchDebounce?.cancel();
|
||||||
|
_searchDebounce = Timer(const Duration(milliseconds: 300), () {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
searchQuery = value.toLowerCase();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFilterButton(BuildContext context, MeshCoreConnector connector) {
|
||||||
|
return DiscoveryContactsFilterMenu(
|
||||||
|
sortOption: sortOption,
|
||||||
|
typeFilter: typeFilter,
|
||||||
|
onSortChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
sortOption = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onTypeFilterChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
typeFilter = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DiscoveryContact> _filterAndSortContacts(
|
||||||
|
List<DiscoveryContact> contacts,
|
||||||
|
MeshCoreConnector connector,
|
||||||
|
) {
|
||||||
|
var filtered = contacts.where((contact) {
|
||||||
|
if (searchQuery.isEmpty) return true;
|
||||||
|
return matchesDiscoveryContactQuery(contact, searchQuery);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
filtered = filtered.where((contact) {
|
||||||
|
return !connector.knownContactKeys.contains(contact.publicKeyHex);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
// Filter out own node from the list
|
||||||
|
if (connector.selfPublicKey != null) {
|
||||||
|
final selfPubKeyHex = pubKeyToHex(connector.selfPublicKey!);
|
||||||
|
filtered = filtered.where((contact) {
|
||||||
|
return contact.publicKeyHex != selfPubKeyHex;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeFilter != ContactTypeFilter.all) {
|
||||||
|
filtered = filtered.where(_matchesTypeFilter).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (sortOption) {
|
||||||
|
case ContactSortOption.lastSeen:
|
||||||
|
filtered.sort((a, b) => b.lastSeen.compareTo(a.lastSeen));
|
||||||
|
break;
|
||||||
|
case ContactSortOption.name:
|
||||||
|
filtered.sort(
|
||||||
|
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _matchesTypeFilter(DiscoveryContact contact) {
|
||||||
|
switch (typeFilter) {
|
||||||
|
case ContactTypeFilter.all:
|
||||||
|
return true;
|
||||||
|
case ContactTypeFilter.users:
|
||||||
|
return contact.type == advTypeChat;
|
||||||
|
case ContactTypeFilter.repeaters:
|
||||||
|
return contact.type == advTypeRepeater;
|
||||||
|
case ContactTypeFilter.rooms:
|
||||||
|
return contact.type == advTypeRoom;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData _getTypeIcon(int type) {
|
||||||
|
switch (type) {
|
||||||
|
case advTypeChat:
|
||||||
|
return Icons.chat;
|
||||||
|
case advTypeRepeater:
|
||||||
|
return Icons.cell_tower;
|
||||||
|
case advTypeRoom:
|
||||||
|
return Icons.group;
|
||||||
|
case advTypeSensor:
|
||||||
|
return Icons.sensors;
|
||||||
|
default:
|
||||||
|
return Icons.device_unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getTypeColor(int type) {
|
||||||
|
switch (type) {
|
||||||
|
case advTypeChat:
|
||||||
|
return Colors.blue;
|
||||||
|
case advTypeRepeater:
|
||||||
|
return Colors.orange;
|
||||||
|
case advTypeRoom:
|
||||||
|
return Colors.purple;
|
||||||
|
case advTypeSensor:
|
||||||
|
return Colors.green;
|
||||||
|
default:
|
||||||
|
return Colors.grey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatLastSeen(BuildContext context, DateTime lastSeen) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final diff = now.difference(lastSeen);
|
||||||
|
|
||||||
|
if (diff.isNegative || diff.inMinutes < 5) {
|
||||||
|
return context.l10n.contacts_lastSeenNow;
|
||||||
|
}
|
||||||
|
if (diff.inMinutes < 60) {
|
||||||
|
return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes);
|
||||||
|
}
|
||||||
|
if (diff.inHours < 24) {
|
||||||
|
final hours = diff.inHours;
|
||||||
|
return hours == 1
|
||||||
|
? context.l10n.contacts_lastSeenHourAgo
|
||||||
|
: context.l10n.contacts_lastSeenHoursAgo(hours);
|
||||||
|
}
|
||||||
|
final days = diff.inDays;
|
||||||
|
return days == 1
|
||||||
|
? context.l10n.contacts_lastSeenDayAgo
|
||||||
|
: context.l10n.contacts_lastSeenDaysAgo(days);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ import '../l10n/app_localizations.dart';
|
|||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../services/app_settings_service.dart';
|
import '../services/app_settings_service.dart';
|
||||||
import '../services/map_tile_cache_service.dart';
|
import '../services/map_tile_cache_service.dart';
|
||||||
|
import '../widgets/adaptive_app_bar_title.dart';
|
||||||
|
|
||||||
class MapCacheScreen extends StatefulWidget {
|
class MapCacheScreen extends StatefulWidget {
|
||||||
const MapCacheScreen({super.key});
|
const MapCacheScreen({super.key});
|
||||||
@@ -224,7 +225,10 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
|||||||
: (_completedTiles / _estimatedTiles).clamp(0.0, 1.0).toDouble();
|
: (_completedTiles / _estimatedTiles).clamp(0.0, 1.0).toDouble();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: Text(l10n.mapCache_title), centerTitle: true),
|
appBar: AppBar(
|
||||||
|
title: AdaptiveAppBarTitle(l10n.mapCache_title),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|||||||
+453
-34
@@ -5,20 +5,23 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:meshcore_open/screens/path_trace_map.dart';
|
import 'package:meshcore_open/screens/path_trace_map.dart';
|
||||||
|
import 'package:meshcore_open/widgets/app_bar.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
|
import '../models/app_settings.dart';
|
||||||
import '../models/channel.dart';
|
import '../models/channel.dart';
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
import '../services/app_settings_service.dart';
|
import '../services/app_settings_service.dart';
|
||||||
|
import '../services/path_history_service.dart';
|
||||||
import '../services/map_marker_service.dart';
|
import '../services/map_marker_service.dart';
|
||||||
import '../services/map_tile_cache_service.dart';
|
import '../services/map_tile_cache_service.dart';
|
||||||
import '../utils/contact_search.dart';
|
import '../utils/contact_search.dart';
|
||||||
import '../utils/route_transitions.dart';
|
import '../utils/route_transitions.dart';
|
||||||
import '../widgets/battery_indicator.dart';
|
|
||||||
import '../widgets/quick_switch_bar.dart';
|
import '../widgets/quick_switch_bar.dart';
|
||||||
|
import '../icons/los_icon.dart';
|
||||||
import 'channels_screen.dart';
|
import 'channels_screen.dart';
|
||||||
import 'chat_screen.dart';
|
import 'chat_screen.dart';
|
||||||
import 'contacts_screen.dart';
|
import 'contacts_screen.dart';
|
||||||
@@ -26,6 +29,7 @@ import '../widgets/repeater_login_dialog.dart';
|
|||||||
import '../widgets/room_login_dialog.dart';
|
import '../widgets/room_login_dialog.dart';
|
||||||
import 'repeater_hub_screen.dart';
|
import 'repeater_hub_screen.dart';
|
||||||
import 'settings_screen.dart';
|
import 'settings_screen.dart';
|
||||||
|
import 'line_of_sight_map_screen.dart';
|
||||||
|
|
||||||
class MapScreen extends StatefulWidget {
|
class MapScreen extends StatefulWidget {
|
||||||
final LatLng? highlightPosition;
|
final LatLng? highlightPosition;
|
||||||
@@ -46,6 +50,8 @@ class MapScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MapScreenState extends State<MapScreen> {
|
class _MapScreenState extends State<MapScreen> {
|
||||||
|
static const double _labelZoomThreshold = 8.5;
|
||||||
|
|
||||||
final MapController _mapController = MapController();
|
final MapController _mapController = MapController();
|
||||||
final MapMarkerService _markerService = MapMarkerService();
|
final MapMarkerService _markerService = MapMarkerService();
|
||||||
final Set<String> _hiddenMarkerIds = {};
|
final Set<String> _hiddenMarkerIds = {};
|
||||||
@@ -58,6 +64,9 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
final List<LatLng> _points = [];
|
final List<LatLng> _points = [];
|
||||||
final List<Polyline> _polylines = [];
|
final List<Polyline> _polylines = [];
|
||||||
bool _legendExpanded = false;
|
bool _legendExpanded = false;
|
||||||
|
bool _showNodeLabels = true;
|
||||||
|
List<_GuessedLocation> _cachedGuessedLocations = [];
|
||||||
|
String _guessedLocationsCacheKey = '';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -105,7 +114,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
double _zoomFromStdDev(double latStdDev, double lonStdDev) {
|
double _zoomFromStdDev(double latStdDev, double lonStdDev) {
|
||||||
final maxSpread = max(latStdDev, lonStdDev);
|
final maxSpread = max(latStdDev, lonStdDev);
|
||||||
if (maxSpread <= 0) return 13.0;
|
if (maxSpread <= 0) return 13.0;
|
||||||
// Approzimate: each zoom level halves the visible area
|
// Approximate: each zoom level halves the visible area
|
||||||
// ~0.01 degrees spread -> zoom 13, ~0.1 -> zoom 10, ~1.0 -> zoom 7
|
// ~0.01 degrees spread -> zoom 13, ~0.1 -> zoom 10, ~1.0 -> zoom 7
|
||||||
final zoom = 10.0 - log(maxSpread * 10 + 1) / ln10 * 3;
|
final zoom = 10.0 - log(maxSpread * 10 + 1) / ln10 * 3;
|
||||||
return zoom.clamp(4.0, 15.0);
|
return zoom.clamp(4.0, 15.0);
|
||||||
@@ -113,8 +122,8 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer2<MeshCoreConnector, AppSettingsService>(
|
return Consumer3<MeshCoreConnector, AppSettingsService, PathHistoryService>(
|
||||||
builder: (context, connector, settingsService, child) {
|
builder: (context, connector, settingsService, pathHistory, child) {
|
||||||
final tileCache = context.read<MapTileCacheService>();
|
final tileCache = context.read<MapTileCacheService>();
|
||||||
final settings = settingsService.settings;
|
final settings = settingsService.settings;
|
||||||
final contacts = connector.contacts;
|
final contacts = connector.contacts;
|
||||||
@@ -154,6 +163,40 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
.where((c) => c.hasLocation)
|
.where((c) => c.hasLocation)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
// All contacts with a known location — used as anchors regardless of
|
||||||
|
// time/key-prefix filters so that repeaters are always available.
|
||||||
|
final allContactsWithLocation = contacts
|
||||||
|
.where((c) => c.hasLocation)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// Compute guessed locations with caching
|
||||||
|
final maxRangeKm = _estimateLoRaRangeKm(connector);
|
||||||
|
final filteredKeys = filteredByKeyPrefix
|
||||||
|
.map((c) => '${c.publicKeyHex}:${c.path.join("-")}')
|
||||||
|
.join(',');
|
||||||
|
final anchorKeys = allContactsWithLocation
|
||||||
|
.map(
|
||||||
|
(c) =>
|
||||||
|
'${c.publicKeyHex}:${c.latitude}:${c.longitude}:${c.path.isNotEmpty ? c.path.last : ""}',
|
||||||
|
)
|
||||||
|
.join(',');
|
||||||
|
final cacheKey =
|
||||||
|
'$filteredKeys|$anchorKeys|${pathHistory.version}:${connector.currentSf}:${connector.currentBwHz}:${connector.currentTxPower}:${settings.mapShowGuessedLocations}';
|
||||||
|
if (cacheKey != _guessedLocationsCacheKey) {
|
||||||
|
_guessedLocationsCacheKey = cacheKey;
|
||||||
|
_cachedGuessedLocations = settings.mapShowGuessedLocations
|
||||||
|
? _computeGuessedLocations(
|
||||||
|
filteredByKeyPrefix,
|
||||||
|
allContactsWithLocation,
|
||||||
|
pathHistory,
|
||||||
|
maxRangeKm,
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
final guessedLocations = settings.mapShowGuessedLocations
|
||||||
|
? _cachedGuessedLocations
|
||||||
|
: <_GuessedLocation>[];
|
||||||
|
|
||||||
_polylines.clear();
|
_polylines.clear();
|
||||||
_polylines.addAll(
|
_polylines.addAll(
|
||||||
_points.length > 1
|
_points.length > 1
|
||||||
@@ -247,6 +290,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
// Re center map after removed markers have loaded
|
// Re center map after removed markers have loaded
|
||||||
if (!_hasInitializedMap && _removedMarkersLoaded) {
|
if (!_hasInitializedMap && _removedMarkersLoaded) {
|
||||||
_hasInitializedMap = true;
|
_hasInitializedMap = true;
|
||||||
|
_showNodeLabels = initialZoom >= _labelZoomThreshold;
|
||||||
if (hasMapContent) {
|
if (hasMapContent) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -262,8 +306,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
canPop: allowBack,
|
canPop: allowBack,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: BatteryIndicator(connector: connector),
|
title: AppBarTitle(context.l10n.map_title),
|
||||||
title: Text(context.l10n.map_title),
|
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
actions: [
|
actions: [
|
||||||
@@ -273,6 +316,47 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
onPressed: () => _startPath(),
|
onPressed: () => _startPath(),
|
||||||
tooltip: context.l10n.contacts_pathTrace,
|
tooltip: context.l10n.contacts_pathTrace,
|
||||||
),
|
),
|
||||||
|
if (!_isBuildingPathTrace)
|
||||||
|
IconButton(
|
||||||
|
icon: const LosIcon(),
|
||||||
|
onPressed: () {
|
||||||
|
final candidates = <LineOfSightEndpoint>[];
|
||||||
|
if (connector.selfLatitude != null &&
|
||||||
|
connector.selfLongitude != null) {
|
||||||
|
candidates.add(
|
||||||
|
LineOfSightEndpoint(
|
||||||
|
label: context.l10n.pathTrace_you,
|
||||||
|
point: LatLng(
|
||||||
|
connector.selfLatitude!,
|
||||||
|
connector.selfLongitude!,
|
||||||
|
),
|
||||||
|
color: Colors.teal,
|
||||||
|
icon: Icons.person_pin_circle,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (final c in contactsWithLocation) {
|
||||||
|
candidates.add(
|
||||||
|
LineOfSightEndpoint(
|
||||||
|
label: c.name,
|
||||||
|
point: LatLng(c.latitude!, c.longitude!),
|
||||||
|
color: _getNodeColor(c.type),
|
||||||
|
icon: _getNodeIcon(c.type),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => LineOfSightMapScreen(
|
||||||
|
title: context.l10n.map_losScreenTitle,
|
||||||
|
candidates: candidates,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
tooltip: context.l10n.map_lineOfSight,
|
||||||
|
),
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
@@ -351,6 +435,14 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
position: latLng,
|
position: latLng,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
onPositionChanged: (camera, hasGesture) {
|
||||||
|
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
||||||
|
if (shouldShow != _showNodeLabels && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_showNodeLabels = shouldShow;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
TileLayer(
|
TileLayer(
|
||||||
@@ -375,7 +467,13 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
size: 34,
|
size: 34,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
..._buildMarkers(contactsWithLocation, settings),
|
if (!_isBuildingPathTrace)
|
||||||
|
...guessedLocations.map(_buildGuessedMarker),
|
||||||
|
..._buildMarkers(
|
||||||
|
contactsWithLocation,
|
||||||
|
settings,
|
||||||
|
showLabels: _showNodeLabels,
|
||||||
|
),
|
||||||
...sharedMarkers.map(_buildSharedMarker),
|
...sharedMarkers.map(_buildSharedMarker),
|
||||||
if (connector.selfLatitude != null &&
|
if (connector.selfLatitude != null &&
|
||||||
connector.selfLongitude != null)
|
connector.selfLongitude != null)
|
||||||
@@ -384,8 +482,8 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
connector.selfLatitude!,
|
connector.selfLatitude!,
|
||||||
connector.selfLongitude!,
|
connector.selfLongitude!,
|
||||||
),
|
),
|
||||||
width: 35,
|
width: 40,
|
||||||
height: 35,
|
height: 40,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -404,24 +502,33 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Text(
|
child: const Icon(
|
||||||
context.l10n.pathTrace_you,
|
Icons.person_pin_circle,
|
||||||
style: const TextStyle(
|
color: Colors.white,
|
||||||
color: Colors.black,
|
size: 20,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (_showNodeLabels &&
|
||||||
|
connector.selfLatitude != null &&
|
||||||
|
connector.selfLongitude != null)
|
||||||
|
_buildNodeLabelMarker(
|
||||||
|
point: LatLng(
|
||||||
|
connector.selfLatitude!,
|
||||||
|
connector.selfLongitude!,
|
||||||
|
),
|
||||||
|
label: context.l10n.pathTrace_you,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (!_isBuildingPathTrace)
|
if (!_isBuildingPathTrace)
|
||||||
_buildLegend(
|
_buildLegend(
|
||||||
contactsWithLocation.length,
|
contactsWithLocation,
|
||||||
|
settings,
|
||||||
sharedMarkers.length,
|
sharedMarkers.length,
|
||||||
|
guessedLocations.length,
|
||||||
),
|
),
|
||||||
if (_isBuildingPathTrace) _buildPathTraceOverlay(),
|
if (_isBuildingPathTrace) _buildPathTraceOverlay(),
|
||||||
],
|
],
|
||||||
@@ -445,20 +552,222 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Marker> _buildMarkers(List<Contact> contacts, settings) {
|
List<_GuessedLocation> _computeGuessedLocations(
|
||||||
|
List<Contact> allContacts,
|
||||||
|
List<Contact> withLocation,
|
||||||
|
PathHistoryService pathHistory,
|
||||||
|
double? maxRangeKm,
|
||||||
|
) {
|
||||||
|
// Index known-location repeaters by their 1-byte hash.
|
||||||
|
// null value = two repeaters share the same hash byte (ambiguous collision).
|
||||||
|
final repeaterByHash = <int, Contact?>{};
|
||||||
|
for (final c in withLocation) {
|
||||||
|
if (c.type == advTypeRepeater) {
|
||||||
|
if (repeaterByHash.containsKey(c.publicKey[0])) {
|
||||||
|
repeaterByHash[c.publicKey[0]] =
|
||||||
|
null; // collision: can't disambiguate
|
||||||
|
} else {
|
||||||
|
repeaterByHash[c.publicKey[0]] = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = <_GuessedLocation>[];
|
||||||
|
|
||||||
|
for (final contact in allContacts) {
|
||||||
|
if (contact.hasLocation) continue;
|
||||||
|
|
||||||
|
final anchorSet = <LatLng>{};
|
||||||
|
|
||||||
|
// Collect the contact-side (last-hop) repeater from every known path.
|
||||||
|
// path = [device-side hop, ..., contact-side hop]
|
||||||
|
// Only path.last is actually within radio range of the contact — using
|
||||||
|
// earlier bytes would anchor against our own side of the network.
|
||||||
|
final pathSets = <List<int>>[
|
||||||
|
contact.path.toList(),
|
||||||
|
...pathHistory
|
||||||
|
.getRecentPaths(contact.publicKeyHex)
|
||||||
|
.map((r) => r.pathBytes),
|
||||||
|
];
|
||||||
|
final lastHopBytes = <int>{};
|
||||||
|
for (final pathBytes in pathSets) {
|
||||||
|
if (pathBytes.isEmpty) continue;
|
||||||
|
final lastHop = pathBytes.last;
|
||||||
|
lastHopBytes.add(lastHop);
|
||||||
|
final r = repeaterByHash[lastHop];
|
||||||
|
if (r != null) anchorSet.add(LatLng(r.latitude!, r.longitude!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: for any last-hop byte with no GPS repeater, average the
|
||||||
|
// positions of contacts with known GPS that share the same last hop.
|
||||||
|
// Those contacts are all adjacent to the same unknown repeater, so their
|
||||||
|
// centroid is a reasonable proxy for its location.
|
||||||
|
for (final byte in lastHopBytes) {
|
||||||
|
if (repeaterByHash.containsKey(byte)) continue;
|
||||||
|
for (final c in withLocation) {
|
||||||
|
if (c.path.isNotEmpty && c.path.last == byte) {
|
||||||
|
anchorSet.add(LatLng(c.latitude!, c.longitude!));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter anchors that are geometrically inconsistent with radio range.
|
||||||
|
// Two anchors more than 2 * maxRange apart cannot both be in direct radio
|
||||||
|
// range of the same node, so isolated outliers are removed.
|
||||||
|
final anchors = maxRangeKm != null && anchorSet.length > 1
|
||||||
|
? _filterConsistentAnchors(anchorSet.toList(), maxRangeKm)
|
||||||
|
: anchorSet.toList();
|
||||||
|
|
||||||
|
if (anchors.isEmpty) continue;
|
||||||
|
|
||||||
|
final LatLng position;
|
||||||
|
if (anchors.length == 1) {
|
||||||
|
// Offset single-anchor guesses so they don't overlap the repeater marker.
|
||||||
|
// Use the contact's public key byte as a deterministic angle seed.
|
||||||
|
const offsetDeg = 0.003; // ~330 m at the equator
|
||||||
|
final angle = (contact.publicKey[1] / 255.0) * 2 * pi;
|
||||||
|
position = LatLng(
|
||||||
|
anchors[0].latitude + offsetDeg * cos(angle),
|
||||||
|
anchors[0].longitude + offsetDeg * sin(angle),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
double lat = 0, lon = 0;
|
||||||
|
for (final a in anchors) {
|
||||||
|
lat += a.latitude;
|
||||||
|
lon += a.longitude;
|
||||||
|
}
|
||||||
|
position = LatLng(lat / anchors.length, lon / anchors.length);
|
||||||
|
}
|
||||||
|
result.add(
|
||||||
|
_GuessedLocation(
|
||||||
|
contact: contact,
|
||||||
|
position: position,
|
||||||
|
highConfidence: anchors.length >= 2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Estimates the free-space maximum LoRa range in km from the connected
|
||||||
|
/// device's current radio parameters. Returns null if parameters are unknown.
|
||||||
|
double? _estimateLoRaRangeKm(MeshCoreConnector connector) {
|
||||||
|
final freqHz = connector.currentFreqHz;
|
||||||
|
final bwHz = connector.currentBwHz;
|
||||||
|
final sf = connector.currentSf;
|
||||||
|
final txPower = connector.currentTxPower;
|
||||||
|
if (freqHz == null || bwHz == null || sf == null || txPower == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// LoRa receiver sensitivity = thermal noise + NF + required demod SNR
|
||||||
|
const noiseFigureDb = 6.0;
|
||||||
|
final thermalNoiseDbm = -174.0 + 10 * log(bwHz.toDouble()) / ln10;
|
||||||
|
final sensitivityDbm =
|
||||||
|
thermalNoiseDbm + noiseFigureDb + _sfToRequiredSnrDb(sf);
|
||||||
|
// FSPL at max range equals link budget:
|
||||||
|
// FSPL = 20*log10(d_m) + 20*log10(f_hz) - 147.55
|
||||||
|
final linkBudgetDb = txPower.toDouble() - sensitivityDbm;
|
||||||
|
final exponent =
|
||||||
|
(linkBudgetDb + 147.55 - 20 * log(freqHz.toDouble()) / ln10) / 20;
|
||||||
|
return pow(10, exponent) / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
double _sfToRequiredSnrDb(int sf) {
|
||||||
|
switch (sf) {
|
||||||
|
case 5:
|
||||||
|
return -2.5;
|
||||||
|
case 6:
|
||||||
|
return -5.0;
|
||||||
|
case 7:
|
||||||
|
return -7.5;
|
||||||
|
case 8:
|
||||||
|
return -10.0;
|
||||||
|
case 9:
|
||||||
|
return -12.5;
|
||||||
|
case 10:
|
||||||
|
return -15.0;
|
||||||
|
case 11:
|
||||||
|
return -17.5;
|
||||||
|
case 12:
|
||||||
|
return -20.0;
|
||||||
|
default:
|
||||||
|
return -10.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes anchors that have no neighbour within 2 * maxRangeKm.
|
||||||
|
/// A node cannot be simultaneously in radio range of two points farther apart
|
||||||
|
/// than twice the expected maximum range.
|
||||||
|
List<LatLng> _filterConsistentAnchors(
|
||||||
|
List<LatLng> anchors,
|
||||||
|
double maxRangeKm,
|
||||||
|
) {
|
||||||
|
const distance = Distance();
|
||||||
|
final maxDistM = maxRangeKm * 2000;
|
||||||
|
return anchors
|
||||||
|
.where((a) => anchors.any((b) => b != a && distance(a, b) <= maxDistM))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Marker _buildGuessedMarker(_GuessedLocation guess) {
|
||||||
|
final color = _getNodeColor(guess.contact.type);
|
||||||
|
return Marker(
|
||||||
|
point: guess.position,
|
||||||
|
width: 35,
|
||||||
|
height: 35,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => _showNodeInfo(
|
||||||
|
context,
|
||||||
|
guess.contact,
|
||||||
|
guessedPosition: guess.position,
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: guess.highConfidence ? 0.55 : 0.30),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.3),
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.not_listed_location,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Marker> _buildMarkers(
|
||||||
|
List<Contact> contacts,
|
||||||
|
settings, {
|
||||||
|
required bool showLabels,
|
||||||
|
}) {
|
||||||
final markers = <Marker>[];
|
final markers = <Marker>[];
|
||||||
|
|
||||||
for (final contact in contacts) {
|
for (final contact in contacts) {
|
||||||
if (!contact.hasLocation) continue;
|
if (!contact.hasLocation) continue;
|
||||||
|
|
||||||
// Apply node type filters
|
// Apply node type filters
|
||||||
if (contact.type == advTypeRepeater && !settings.mapShowRepeaters) {
|
if (contact.type == advTypeRepeater &&
|
||||||
|
(!settings.mapShowRepeaters && !_isBuildingPathTrace)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (contact.type == advTypeChat &&
|
||||||
|
!(settings.mapShowChatNodes && !_isBuildingPathTrace)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (contact.type == advTypeChat && !settings.mapShowChatNodes) continue;
|
|
||||||
if (contact.type != advTypeChat &&
|
if (contact.type != advTypeChat &&
|
||||||
contact.type != advTypeRepeater &&
|
contact.type != advTypeRepeater &&
|
||||||
!settings.mapShowOtherNodes) {
|
(!settings.mapShowOtherNodes && !_isBuildingPathTrace)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -500,11 +809,54 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
markers.add(marker);
|
markers.add(marker);
|
||||||
|
if (showLabels) {
|
||||||
|
markers.add(
|
||||||
|
_buildNodeLabelMarker(
|
||||||
|
point: LatLng(contact.latitude!, contact.longitude!),
|
||||||
|
label: contact.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return markers;
|
return markers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Marker _buildNodeLabelMarker({required LatLng point, required String label}) {
|
||||||
|
return Marker(
|
||||||
|
point: point,
|
||||||
|
width: 120,
|
||||||
|
height: 24,
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: Transform.translate(
|
||||||
|
offset: const Offset(0, -20),
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black54,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Color _getNodeColor(int type) {
|
Color _getNodeColor(int type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case advTypeChat:
|
case advTypeChat:
|
||||||
@@ -535,7 +887,27 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLegend(int nodeCount, int markerCount) {
|
Widget _buildLegend(
|
||||||
|
List<Contact> contactsWithLocation,
|
||||||
|
settings,
|
||||||
|
int markerCount,
|
||||||
|
int guessedCount,
|
||||||
|
) {
|
||||||
|
int nodeCount = 0;
|
||||||
|
for (final contact in contactsWithLocation) {
|
||||||
|
// Apply node type filters
|
||||||
|
if (contact.type == advTypeRepeater && !settings.mapShowRepeaters) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (contact.type == advTypeChat && !settings.mapShowChatNodes) continue;
|
||||||
|
if (contact.type != advTypeChat &&
|
||||||
|
contact.type != advTypeRepeater &&
|
||||||
|
!settings.mapShowOtherNodes) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
nodeCount++;
|
||||||
|
}
|
||||||
|
|
||||||
return Positioned(
|
return Positioned(
|
||||||
top: 16,
|
top: 16,
|
||||||
right: 16,
|
right: 16,
|
||||||
@@ -559,7 +931,12 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
context.l10n.map_nodesCount(nodeCount),
|
context.l10n.map_nodesCount(
|
||||||
|
nodeCount +
|
||||||
|
(settings.mapShowGuessedLocations
|
||||||
|
? guessedCount
|
||||||
|
: 0),
|
||||||
|
),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
@@ -627,6 +1004,12 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
context.l10n.map_pinPublic,
|
context.l10n.map_pinPublic,
|
||||||
Colors.orange,
|
Colors.orange,
|
||||||
),
|
),
|
||||||
|
if (settings.mapShowGuessedLocations && guessedCount > 0)
|
||||||
|
_buildLegendItem(
|
||||||
|
Icons.not_listed_location,
|
||||||
|
context.l10n.map_guessedLocation,
|
||||||
|
Colors.grey,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -815,7 +1198,11 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showNodeInfo(BuildContext context, Contact contact) {
|
void _showNodeInfo(
|
||||||
|
BuildContext context,
|
||||||
|
Contact contact, {
|
||||||
|
LatLng? guessedPosition,
|
||||||
|
}) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (dialogContext) => AlertDialog(
|
builder: (dialogContext) => AlertDialog(
|
||||||
@@ -826,7 +1213,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
color: _getNodeColor(contact.type),
|
color: _getNodeColor(contact.type),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(child: Text(contact.name)),
|
Expanded(child: SelectableText(contact.name)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
content: Column(
|
content: Column(
|
||||||
@@ -835,10 +1222,16 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
children: [
|
children: [
|
||||||
_buildInfoRow('Type', contact.typeLabel),
|
_buildInfoRow('Type', contact.typeLabel),
|
||||||
_buildInfoRow('Path', contact.pathLabel),
|
_buildInfoRow('Path', contact.pathLabel),
|
||||||
_buildInfoRow(
|
if (contact.hasLocation)
|
||||||
'Location',
|
_buildInfoRow(
|
||||||
'${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}',
|
'Location',
|
||||||
),
|
'${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}',
|
||||||
|
)
|
||||||
|
else if (guessedPosition != null)
|
||||||
|
_buildInfoRow(
|
||||||
|
'Est. Location',
|
||||||
|
'~${guessedPosition.latitude.toStringAsFixed(6)}, ${guessedPosition.longitude.toStringAsFixed(6)}',
|
||||||
|
),
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
context.l10n.map_lastSeen,
|
context.l10n.map_lastSeen,
|
||||||
_formatLastSeen(contact.lastSeen),
|
_formatLastSeen(contact.lastSeen),
|
||||||
@@ -997,7 +1390,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(value, style: const TextStyle(fontSize: 14)),
|
SelectableText(value, style: const TextStyle(fontSize: 14)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1164,7 +1557,8 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
|
padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: context.l10n.contacts_searchContacts,
|
hintText:
|
||||||
|
context.l10n.contacts_searchContactsNoNumber,
|
||||||
prefixIcon: const Icon(Icons.search),
|
prefixIcon: const Icon(Icons.search),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
@@ -1343,6 +1737,14 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
},
|
},
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
),
|
),
|
||||||
|
CheckboxListTile(
|
||||||
|
title: Text(context.l10n.map_showGuessedLocations),
|
||||||
|
value: settings.mapShowGuessedLocations,
|
||||||
|
onChanged: (value) {
|
||||||
|
service.setMapShowGuessedLocations(value ?? true);
|
||||||
|
},
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
context.l10n.map_keyPrefix,
|
context.l10n.map_keyPrefix,
|
||||||
@@ -1520,6 +1922,9 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
|
|
||||||
Widget _buildPathTraceOverlay() {
|
Widget _buildPathTraceOverlay() {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
|
final isImperial =
|
||||||
|
context.read<AppSettingsService>().settings.unitSystem ==
|
||||||
|
UnitSystem.imperial;
|
||||||
return Positioned(
|
return Positioned(
|
||||||
top: 16,
|
top: 16,
|
||||||
left: 16,
|
left: 16,
|
||||||
@@ -1540,7 +1945,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
if (_pathTrace.isNotEmpty)
|
if (_pathTrace.isNotEmpty)
|
||||||
Text(
|
Text(
|
||||||
"${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points))}",
|
"${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points), isImperial: isImperial)}",
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey[700]),
|
style: TextStyle(fontSize: 12, color: Colors.grey[700]),
|
||||||
),
|
),
|
||||||
SelectableText(
|
SelectableText(
|
||||||
@@ -1550,8 +1955,10 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
style: TextStyle(fontSize: 18),
|
style: TextStyle(fontSize: 18),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Row(
|
Wrap(
|
||||||
mainAxisSize: MainAxisSize.min,
|
alignment: WrapAlignment.center,
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
children: [
|
children: [
|
||||||
if (_pathTrace.isNotEmpty)
|
if (_pathTrace.isNotEmpty)
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
@@ -1601,6 +2008,18 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _GuessedLocation {
|
||||||
|
final Contact contact;
|
||||||
|
final LatLng position;
|
||||||
|
final bool highConfidence;
|
||||||
|
|
||||||
|
_GuessedLocation({
|
||||||
|
required this.contact,
|
||||||
|
required this.position,
|
||||||
|
required this.highConfidence,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class _MarkerPayload {
|
class _MarkerPayload {
|
||||||
final LatLng position;
|
final LatLng position;
|
||||||
final String label;
|
final String label;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:meshcore_open/utils/app_logger.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
@@ -11,28 +12,28 @@ import '../services/repeater_command_service.dart';
|
|||||||
import '../widgets/path_management_dialog.dart';
|
import '../widgets/path_management_dialog.dart';
|
||||||
import '../widgets/snr_indicator.dart';
|
import '../widgets/snr_indicator.dart';
|
||||||
|
|
||||||
class NeighboursScreen extends StatefulWidget {
|
class NeighborsScreen extends StatefulWidget {
|
||||||
final Contact repeater;
|
final Contact repeater;
|
||||||
final String password;
|
final String password;
|
||||||
|
|
||||||
const NeighboursScreen({
|
const NeighborsScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.repeater,
|
required this.repeater,
|
||||||
required this.password,
|
required this.password,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<NeighboursScreen> createState() => _NeighboursScreenState();
|
State<NeighborsScreen> createState() => _NeighborsScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NeighboursScreenState extends State<NeighboursScreen> {
|
class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||||
static const int _reqNeighboursKeyLen = 4;
|
static const int _reqNeighborsKeyLen = 4;
|
||||||
static const int _statusPayloadOffset = 8;
|
static const int _statusPayloadOffset = 8;
|
||||||
static const int _statusStatsSize = 52;
|
static const int _statusStatsSize = 52;
|
||||||
static const int _statusResponseBytes =
|
static const int _statusResponseBytes =
|
||||||
_statusPayloadOffset + _statusStatsSize;
|
_statusPayloadOffset + _statusStatsSize;
|
||||||
Uint8List _tagData = Uint8List(4);
|
Uint8List _tagData = Uint8List(4);
|
||||||
int _neighbourCount = 0;
|
int _neighborCount = 0;
|
||||||
|
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
bool _isLoaded = false;
|
bool _isLoaded = false;
|
||||||
@@ -41,7 +42,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
StreamSubscription<Uint8List>? _frameSubscription;
|
StreamSubscription<Uint8List>? _frameSubscription;
|
||||||
RepeaterCommandService? _commandService;
|
RepeaterCommandService? _commandService;
|
||||||
PathSelection? _pendingStatusSelection;
|
PathSelection? _pendingStatusSelection;
|
||||||
List<Map<String, dynamic>>? _parsedNeighbours;
|
List<Map<String, dynamic>>? _parsedNeighbors;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -49,7 +50,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
_commandService = RepeaterCommandService(connector);
|
_commandService = RepeaterCommandService(connector);
|
||||||
_setupMessageListener();
|
_setupMessageListener();
|
||||||
_loadNeighbours();
|
_loadNeighbors();
|
||||||
_hasData = false;
|
_hasData = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,13 +63,12 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
|
|
||||||
if (frame[0] == respCodeSent) {
|
if (frame[0] == respCodeSent) {
|
||||||
_tagData = frame.sublist(2, 6);
|
_tagData = frame.sublist(2, 6);
|
||||||
//_timeEstment = frame.buffer.asByteData().getUint32(6, Endian.little);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's a binary response
|
// Check if it's a binary response
|
||||||
if (frame[0] == pushCodeBinaryResponse &&
|
if (frame[0] == pushCodeBinaryResponse &&
|
||||||
listEquals(frame.sublist(2, 6), _tagData)) {
|
listEquals(frame.sublist(2, 6), _tagData)) {
|
||||||
_handleNeighboursResponse(connector, frame.sublist(6));
|
_handleNeighborsResponse(connector, frame.sublist(6));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -91,65 +91,77 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
return '${h}h ${m2}m';
|
return '${h}h ${m2}m';
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<Map<String, dynamic>> parseNeighboursData(
|
static List<Map<String, dynamic>> parseNeighborsData(
|
||||||
BufferReader buffer,
|
BufferReader buffer,
|
||||||
int resultsCount,
|
int resultsCount,
|
||||||
) {
|
) {
|
||||||
final Map<int, Map<String, dynamic>> neighbours = {};
|
final Map<int, Map<String, dynamic>> neighbors = {};
|
||||||
for (var i = 0; i < resultsCount; i++) {
|
try {
|
||||||
final neighbourData = neighbours.putIfAbsent(
|
for (var i = 0; i < resultsCount; i++) {
|
||||||
i,
|
final neighborData = neighbors.putIfAbsent(
|
||||||
() => {
|
i,
|
||||||
'contact': null,
|
() => {
|
||||||
'publicKey': <Uint8List>{},
|
'contact': null,
|
||||||
'lastHeard': <int>{},
|
'publicKey': <Uint8List>{},
|
||||||
'snr': <double>{},
|
'lastHeard': <int>{},
|
||||||
},
|
'snr': <double>{},
|
||||||
);
|
},
|
||||||
neighbourData['publicKey'] = buffer.readBytes(_reqNeighboursKeyLen);
|
);
|
||||||
neighbourData['lastHeard'] = buffer.readUInt32LE();
|
neighborData['publicKey'] = buffer.readBytes(_reqNeighborsKeyLen);
|
||||||
neighbourData['snr'] = buffer.readInt8() / 4.0;
|
neighborData['lastHeard'] = buffer.readUInt32LE();
|
||||||
}
|
neighborData['snr'] = buffer.readInt8() / 4.0;
|
||||||
|
}
|
||||||
|
|
||||||
return neighbours.values.toList();
|
return neighbors.values.toList();
|
||||||
|
} catch (e) {
|
||||||
|
appLogger.error(
|
||||||
|
'Error parsing neighbors data: $e',
|
||||||
|
tag: 'NeighborsScreen',
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleNeighboursResponse(MeshCoreConnector connector, Uint8List frame) {
|
void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) {
|
||||||
final buffer = BufferReader(frame);
|
final buffer = BufferReader(frame);
|
||||||
final neighbourCount = buffer.readUInt16LE();
|
try {
|
||||||
final parsedNeighbours = parseNeighboursData(buffer, buffer.readUInt16LE());
|
final neighborCount = buffer.readUInt16LE();
|
||||||
connector.contacts.where((c) => c.type == advTypeRepeater).forEach((
|
final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE());
|
||||||
repeater,
|
connector.contacts.where((c) => c.type == advTypeRepeater).forEach((
|
||||||
) {
|
repeater,
|
||||||
for (var neighbourData in parsedNeighbours) {
|
) {
|
||||||
final publicKey = neighbourData['publicKey'];
|
for (var neighborData in parsedNeighbors) {
|
||||||
if (listEquals(
|
final publicKey = neighborData['publicKey'];
|
||||||
repeater.publicKey.sublist(0, _reqNeighboursKeyLen),
|
if (listEquals(
|
||||||
publicKey,
|
repeater.publicKey.sublist(0, _reqNeighborsKeyLen),
|
||||||
)) {
|
publicKey,
|
||||||
neighbourData['contact'] = repeater;
|
)) {
|
||||||
|
neighborData['contact'] = repeater;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_parsedNeighbours = parsedNeighbours;
|
_parsedNeighbors = parsedNeighbors;
|
||||||
_neighbourCount = neighbourCount;
|
_neighborCount = neighborCount;
|
||||||
});
|
});
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(context.l10n.neighbors_receivedData),
|
content: Text(context.l10n.neighbors_receivedData),
|
||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.green,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
_statusTimeout?.cancel();
|
_statusTimeout?.cancel();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
_isLoaded = true;
|
_isLoaded = true;
|
||||||
_hasData = true;
|
_hasData = true;
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
appLogger.error('Error handling neighbors response: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Contact _resolveRepeater(MeshCoreConnector connector) {
|
Contact _resolveRepeater(MeshCoreConnector connector) {
|
||||||
@@ -159,7 +171,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadNeighbours() async {
|
Future<void> _loadNeighbors() async {
|
||||||
if (_commandService == null) return;
|
if (_commandService == null) return;
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -172,17 +184,17 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
final selection = await connector.preparePathForContactSend(repeater);
|
final selection = await connector.preparePathForContactSend(repeater);
|
||||||
_pendingStatusSelection = selection;
|
_pendingStatusSelection = selection;
|
||||||
|
|
||||||
//[version][number of requested neighbours][offset_16bit][order by][len of public key]
|
//[version][number of requested neighbors][offset_16bit][order by][len of public key]
|
||||||
final frame = buildSendBinaryReq(
|
final frame = buildSendBinaryReq(
|
||||||
repeater.publicKey,
|
repeater.publicKey,
|
||||||
payload: Uint8List.fromList([
|
payload: Uint8List.fromList([
|
||||||
reqTypeGetNeighbours,
|
reqTypeGetNeighbors,
|
||||||
0x00,
|
0x00,
|
||||||
0x0F,
|
0x0F,
|
||||||
0x00,
|
0x00,
|
||||||
0x00,
|
0x00,
|
||||||
0x00,
|
0x00,
|
||||||
_reqNeighboursKeyLen,
|
_reqNeighborsKeyLen,
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
await connector.sendFrame(frame);
|
await connector.sendFrame(frame);
|
||||||
@@ -258,7 +270,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
l10n.neighbors_repeatersNeighbours,
|
l10n.neighbors_repeatersNeighbors,
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
@@ -345,7 +357,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: const Icon(Icons.refresh),
|
: const Icon(Icons.refresh),
|
||||||
onPressed: _isLoading ? null : _loadNeighbours,
|
onPressed: _isLoading ? null : _loadNeighbors,
|
||||||
tooltip: l10n.repeater_refresh,
|
tooltip: l10n.repeater_refresh,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -353,13 +365,13 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
child: RefreshIndicator(
|
child: RefreshIndicator(
|
||||||
onRefresh: _loadNeighbours,
|
onRefresh: _loadNeighbors,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
children: [
|
children: [
|
||||||
if (!_isLoaded &&
|
if (!_isLoaded &&
|
||||||
!_hasData &&
|
!_hasData &&
|
||||||
(_parsedNeighbours == null || _parsedNeighbours!.isEmpty))
|
(_parsedNeighbors == null || _parsedNeighbors!.isEmpty))
|
||||||
Center(
|
Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
l10n.neighbors_noData,
|
l10n.neighbors_noData,
|
||||||
@@ -368,10 +380,9 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
),
|
),
|
||||||
if (_isLoaded ||
|
if (_isLoaded ||
|
||||||
_hasData &&
|
_hasData &&
|
||||||
!(_parsedNeighbours == null ||
|
!(_parsedNeighbors == null || _parsedNeighbors!.isEmpty))
|
||||||
_parsedNeighbours!.isEmpty))
|
_buildNeighborsInfoCard(
|
||||||
_buildNeighboursInfoCard(
|
"${l10n.repeater_neighbors} - $_neighborCount",
|
||||||
"${l10n.repeater_neighbours} - $_neighbourCount",
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -380,7 +391,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildNeighboursInfoCard(String title) {
|
Widget _buildNeighborsInfoCard(String title) {
|
||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
return Card(
|
return Card(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -405,7 +416,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
for (final entry in _parsedNeighbours!.asMap().entries)
|
for (final entry in _parsedNeighbors!.asMap().entries)
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
entry.value['contact'] != null
|
entry.value['contact'] != null
|
||||||
? entry.value['contact'].name
|
? entry.value['contact'].name
|
||||||
@@ -430,6 +441,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
double snr,
|
double snr,
|
||||||
int spreadingFactor,
|
int spreadingFactor,
|
||||||
) {
|
) {
|
||||||
|
final snrUi = snrUiFromSNR(snr, spreadingFactor);
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -443,9 +455,15 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||||
),
|
),
|
||||||
subtitle: Text(value),
|
subtitle: Text(value),
|
||||||
trailing: SNRIcon(
|
trailing: Column(
|
||||||
snr: snr,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
snrLevels: getSNRfromSF(spreadingFactor),
|
children: [
|
||||||
|
Icon(snrUi.icon, color: snrUi.color, size: 18.0),
|
||||||
|
Text(
|
||||||
|
snrUi.text,
|
||||||
|
style: TextStyle(fontSize: 10, color: snrUi.color),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
+391
-134
@@ -8,8 +8,11 @@ import 'package:latlong2/latlong.dart';
|
|||||||
import 'package:meshcore_open/connector/meshcore_connector.dart';
|
import 'package:meshcore_open/connector/meshcore_connector.dart';
|
||||||
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
||||||
import 'package:meshcore_open/l10n/l10n.dart';
|
import 'package:meshcore_open/l10n/l10n.dart';
|
||||||
|
import 'package:meshcore_open/models/app_settings.dart';
|
||||||
import 'package:meshcore_open/models/contact.dart';
|
import 'package:meshcore_open/models/contact.dart';
|
||||||
|
import 'package:meshcore_open/services/app_settings_service.dart';
|
||||||
import 'package:meshcore_open/services/map_tile_cache_service.dart';
|
import 'package:meshcore_open/services/map_tile_cache_service.dart';
|
||||||
|
import 'package:meshcore_open/utils/app_logger.dart';
|
||||||
import 'package:meshcore_open/widgets/snr_indicator.dart';
|
import 'package:meshcore_open/widgets/snr_indicator.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -26,13 +29,16 @@ double getPathDistanceMeters(List<LatLng> points) {
|
|||||||
return distanceMeters;
|
return distanceMeters;
|
||||||
}
|
}
|
||||||
|
|
||||||
String formatDistance(double distanceMeters) {
|
String formatDistance(double distanceMeters, {required bool isImperial}) {
|
||||||
return '(${(distanceMeters / 1609.34).toStringAsFixed(2)} Miles / ${(distanceMeters / 1000).toStringAsFixed(2)} Km)';
|
if (isImperial) {
|
||||||
|
return '(${(distanceMeters / 1609.34).toStringAsFixed(2)} mi)';
|
||||||
|
}
|
||||||
|
return '(${(distanceMeters / 1000).toStringAsFixed(2)} km)';
|
||||||
}
|
}
|
||||||
|
|
||||||
class PathTraceData {
|
class PathTraceData {
|
||||||
final Uint8List pathData;
|
final Uint8List pathData;
|
||||||
final Uint8List snrData;
|
final List<double> snrData;
|
||||||
final Map<int, Contact> pathContacts;
|
final Map<int, Contact> pathContacts;
|
||||||
|
|
||||||
PathTraceData({
|
PathTraceData({
|
||||||
@@ -45,15 +51,19 @@ class PathTraceData {
|
|||||||
class PathTraceMapScreen extends StatefulWidget {
|
class PathTraceMapScreen extends StatefulWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final Uint8List path;
|
final Uint8List path;
|
||||||
|
final int? repeaterId;
|
||||||
final bool flipPathRound;
|
final bool flipPathRound;
|
||||||
final bool reversePathRound;
|
final bool reversePathRound;
|
||||||
|
final Contact? targetContact;
|
||||||
|
|
||||||
const PathTraceMapScreen({
|
const PathTraceMapScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.path,
|
required this.path,
|
||||||
|
this.repeaterId,
|
||||||
this.flipPathRound = false,
|
this.flipPathRound = false,
|
||||||
this.reversePathRound = false,
|
this.reversePathRound = false,
|
||||||
|
this.targetContact,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -61,6 +71,8 @@ class PathTraceMapScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||||
|
static const double _labelZoomThreshold = 8.5;
|
||||||
|
|
||||||
StreamSubscription<Uint8List>? _frameSubscription;
|
StreamSubscription<Uint8List>? _frameSubscription;
|
||||||
Timer? _timeoutTimer;
|
Timer? _timeoutTimer;
|
||||||
|
|
||||||
@@ -68,6 +80,11 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
bool _failed2Loaded = false;
|
bool _failed2Loaded = false;
|
||||||
bool _hasData = false;
|
bool _hasData = false;
|
||||||
PathTraceData? _traceData;
|
PathTraceData? _traceData;
|
||||||
|
// Inferred positions for hops that have no GPS location, keyed by hop byte.
|
||||||
|
Map<int, LatLng> _inferredHopPositions = {};
|
||||||
|
// Endpoint position for the target contact (GPS or guessed).
|
||||||
|
LatLng? _targetContactPosition;
|
||||||
|
bool _targetContactIsGuessed = false;
|
||||||
List<LatLng> _points = <LatLng>[];
|
List<LatLng> _points = <LatLng>[];
|
||||||
List<Polyline> _polylines = [];
|
List<Polyline> _polylines = [];
|
||||||
LatLng? _initialCenter = LatLng(0, 0);
|
LatLng? _initialCenter = LatLng(0, 0);
|
||||||
@@ -75,6 +92,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
LatLngBounds? _bounds;
|
LatLngBounds? _bounds;
|
||||||
ValueKey<String> _mapKey = const ValueKey('initial');
|
ValueKey<String> _mapKey = const ValueKey('initial');
|
||||||
double _pathDistanceMeters = 0.0;
|
double _pathDistanceMeters = 0.0;
|
||||||
|
bool _showNodeLabels = true;
|
||||||
|
|
||||||
String _formatPathPrefixes(Uint8List pathBytes) {
|
String _formatPathPrefixes(Uint8List pathBytes) {
|
||||||
return pathBytes
|
return pathBytes
|
||||||
@@ -96,7 +114,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint8List addReturnpath(Uint8List pathBytes) {
|
Uint8List addReturnPath(Uint8List pathBytes) {
|
||||||
Uint8List? traceBytes;
|
Uint8List? traceBytes;
|
||||||
final len = (pathBytes.length + pathBytes.length - 1);
|
final len = (pathBytes.length + pathBytes.length - 1);
|
||||||
traceBytes = Uint8List(len);
|
traceBytes = Uint8List(len);
|
||||||
@@ -124,7 +142,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
: widget.path;
|
: widget.path;
|
||||||
|
|
||||||
if (widget.flipPathRound) {
|
if (widget.flipPathRound) {
|
||||||
path = addReturnpath(pathTmp);
|
path = addReturnPath(pathTmp);
|
||||||
} else {
|
} else {
|
||||||
path = pathTmp;
|
path = pathTmp;
|
||||||
}
|
}
|
||||||
@@ -146,42 +164,57 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
_frameSubscription = connector.receivedFrames.listen((frame) {
|
_frameSubscription = connector.receivedFrames.listen((frame) {
|
||||||
if (frame.isEmpty) return;
|
if (frame.isEmpty) return;
|
||||||
final frameBuffer = BufferReader(frame);
|
final frameBuffer = BufferReader(frame);
|
||||||
final code = frameBuffer.readUInt8();
|
try {
|
||||||
|
final code = frameBuffer.readUInt8();
|
||||||
|
|
||||||
if (code == respCodeSent) {
|
if (code == respCodeSent) {
|
||||||
frameBuffer.skipBytes(1); //reserved
|
frameBuffer.skipBytes(1); //reserved
|
||||||
tagData = frameBuffer.readBytes(4);
|
tagData = frameBuffer.readBytes(4);
|
||||||
final timeoutSeconds = frameBuffer.readUInt32LE();
|
final timeoutMilliseconds = frameBuffer.readUInt32LE();
|
||||||
|
|
||||||
// Start timeout timer for trace response
|
// Start timeout timer for trace response
|
||||||
_timeoutTimer?.cancel();
|
_timeoutTimer?.cancel();
|
||||||
_timeoutTimer = Timer(Duration(milliseconds: timeoutSeconds), () {
|
_timeoutTimer = Timer(
|
||||||
|
Duration(milliseconds: timeoutMilliseconds),
|
||||||
|
() {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_failed2Loaded = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code == respCodeErr) {
|
||||||
|
_timeoutTimer?.cancel();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
_failed2Loaded = true;
|
_failed2Loaded = true;
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (code == respCodeErr) {
|
// Check if it's a binary response
|
||||||
|
if (frame.length > 8 &&
|
||||||
|
code == pushCodeTraceData &&
|
||||||
|
listEquals(frame.sublist(4, 8), tagData)) {
|
||||||
|
_timeoutTimer?.cancel();
|
||||||
|
if (!mounted) return;
|
||||||
|
frameBuffer.skipBytes(3); //reserved + path length + flag
|
||||||
|
if (listEquals(frameBuffer.readBytes(4), tagData)) {
|
||||||
|
_handleTraceResponse(frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
_timeoutTimer?.cancel();
|
_timeoutTimer?.cancel();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
_failed2Loaded = true;
|
_failed2Loaded = true;
|
||||||
});
|
});
|
||||||
}
|
// Handle any parsing errors gracefully
|
||||||
// Check if it's a binary response
|
appLogger.error('Error parsing frame: $e', tag: 'PathTraceMapScreen');
|
||||||
if (frame.length > 8 &&
|
|
||||||
code == pushCodeTraceData &&
|
|
||||||
listEquals(frame.sublist(4, 8), tagData)) {
|
|
||||||
_timeoutTimer?.cancel();
|
|
||||||
if (!mounted) return;
|
|
||||||
frameBuffer.skipBytes(3); //reserved + path length + flag
|
|
||||||
if (listEquals(frameBuffer.readBytes(4), tagData)) {
|
|
||||||
_handleTraceResponse(frame);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -190,69 +223,157 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
|
|
||||||
final buffer = BufferReader(frame);
|
final buffer = BufferReader(frame);
|
||||||
buffer.skipBytes(2); // Skip push code and reserved byte
|
try {
|
||||||
int pathLength = buffer.readUInt8();
|
buffer.skipBytes(2); // Skip push code and reserved byte
|
||||||
buffer.skipBytes(5); // Skip Flag byte and tag data
|
int pathLength = buffer.readUInt8();
|
||||||
buffer.skipBytes(4); // Skip auth code
|
buffer.skipBytes(5); // Skip Flag byte and tag data
|
||||||
Uint8List pathData = buffer.readBytes(pathLength);
|
buffer.skipBytes(4); // Skip auth code
|
||||||
Uint8List snrData = buffer.readRemainingBytes();
|
Uint8List pathData = buffer.readBytes(pathLength);
|
||||||
|
List<double> snrData = buffer
|
||||||
|
.readRemainingBytes()
|
||||||
|
.map((snr) => snr.toSigned(8).toDouble() / 4)
|
||||||
|
.toList();
|
||||||
|
|
||||||
Map<int, Contact> pathContacts = {};
|
Map<int, Contact> pathContacts = {};
|
||||||
|
|
||||||
connector.contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
|
connector.contacts.where((c) => c.type != advTypeChat).forEach((
|
||||||
for (var repeaterData in pathData) {
|
repeater,
|
||||||
if (listEquals(
|
) {
|
||||||
repeater.publicKey.sublist(0, 1),
|
for (var repeaterData in pathData) {
|
||||||
Uint8List.fromList([repeaterData]),
|
if (listEquals(
|
||||||
)) {
|
repeater.publicKey.sublist(0, 1),
|
||||||
pathContacts[repeaterData] = repeater;
|
Uint8List.fromList([repeaterData]),
|
||||||
|
)) {
|
||||||
|
pathContacts[repeaterData] = repeater;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// For hops with no GPS contact, infer position from other contacts
|
||||||
|
// with known GPS that share the same last-hop byte.
|
||||||
|
final Map<int, LatLng> inferredPositions = {};
|
||||||
|
for (final hop in pathData) {
|
||||||
|
final contact = pathContacts[hop];
|
||||||
|
if (contact != null && contact.hasLocation) continue;
|
||||||
|
final peers = connector.contacts
|
||||||
|
.where(
|
||||||
|
(c) => c.hasLocation && c.path.isNotEmpty && c.path.last == hop,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
if (peers.isNotEmpty) {
|
||||||
|
final lat =
|
||||||
|
peers.map((c) => c.latitude!).reduce((a, b) => a + b) /
|
||||||
|
peers.length;
|
||||||
|
final lon =
|
||||||
|
peers.map((c) => c.longitude!).reduce((a, b) => a + b) /
|
||||||
|
peers.length;
|
||||||
|
inferredPositions[hop] = LatLng(lat, lon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
_hasData = true;
|
_hasData = true;
|
||||||
_traceData = PathTraceData(
|
_inferredHopPositions = inferredPositions;
|
||||||
pathData: pathData,
|
_traceData = PathTraceData(
|
||||||
snrData: snrData,
|
pathData: pathData,
|
||||||
pathContacts: pathContacts,
|
snrData: snrData,
|
||||||
);
|
pathContacts: pathContacts,
|
||||||
_points = <LatLng>[];
|
);
|
||||||
_points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
// Compute endpoint position for the target contact.
|
||||||
for (final hop in _traceData!.pathData) {
|
LatLng? targetPos;
|
||||||
final contact = _traceData!.pathContacts[hop];
|
bool targetGuessed = false;
|
||||||
if (contact != null &&
|
final target = widget.targetContact;
|
||||||
contact.hasLocation &&
|
if (target != null) {
|
||||||
contact.latitude != null &&
|
if (target.hasLocation) {
|
||||||
contact.longitude != null) {
|
targetPos = LatLng(target.latitude!, target.longitude!);
|
||||||
_points.add(LatLng(contact.latitude!, contact.longitude!));
|
} else if (pathData.isNotEmpty) {
|
||||||
|
// Infer from the last hop: average GPS contacts sharing that hop.
|
||||||
|
// For a round-trip path (flipPathRound), the target-side hop sits
|
||||||
|
// in the middle of the symmetric sequence; .last is the local side.
|
||||||
|
final lastHop = (widget.flipPathRound && pathData.length > 1)
|
||||||
|
? pathData[(pathData.length - 1) ~/ 2]
|
||||||
|
: pathData.last;
|
||||||
|
final peers = connector.contacts
|
||||||
|
.where(
|
||||||
|
(c) =>
|
||||||
|
c.hasLocation &&
|
||||||
|
c.path.isNotEmpty &&
|
||||||
|
c.path.last == lastHop,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
if (peers.isNotEmpty) {
|
||||||
|
final lat =
|
||||||
|
peers.map((c) => c.latitude!).reduce((a, b) => a + b) /
|
||||||
|
peers.length;
|
||||||
|
final lon =
|
||||||
|
peers.map((c) => c.longitude!).reduce((a, b) => a + b) /
|
||||||
|
peers.length;
|
||||||
|
const offsetDeg = 0.003;
|
||||||
|
final angle = (target.publicKey[1] / 255.0) * 2 * pi;
|
||||||
|
targetPos = LatLng(
|
||||||
|
lat + offsetDeg * cos(angle),
|
||||||
|
lon + offsetDeg * sin(angle),
|
||||||
|
);
|
||||||
|
targetGuessed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
_targetContactPosition = targetPos;
|
||||||
_polylines = _points.length > 1
|
_targetContactIsGuessed = targetGuessed;
|
||||||
? [
|
|
||||||
Polyline(
|
|
||||||
points: _points,
|
|
||||||
strokeWidth: 4,
|
|
||||||
color: Colors.blueAccent,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
: <Polyline>[];
|
|
||||||
|
|
||||||
_initialCenter = _points.isNotEmpty ? _points.first : const LatLng(0, 0);
|
_points = <LatLng>[];
|
||||||
_initialZoom = _points.isNotEmpty ? 13.0 : 2.0;
|
_points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
||||||
_bounds = _points.length > 1 ? LatLngBounds.fromPoints(_points) : null;
|
for (final hop in _traceData!.pathData) {
|
||||||
_mapKey = ValueKey(
|
final contact = _traceData!.pathContacts[hop];
|
||||||
'${context.l10n.pathTrace_you},${_formatPathPrefixes(_traceData!.pathData)}',
|
if (contact != null && contact.hasLocation) {
|
||||||
|
_points.add(LatLng(contact.latitude!, contact.longitude!));
|
||||||
|
} else {
|
||||||
|
final inferred = inferredPositions[hop];
|
||||||
|
if (inferred != null) _points.add(inferred);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetPos != null) _points.add(targetPos);
|
||||||
|
_polylines = _points.length > 1
|
||||||
|
? [
|
||||||
|
Polyline(
|
||||||
|
points: _points,
|
||||||
|
strokeWidth: 4,
|
||||||
|
color: Colors.blueAccent,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: <Polyline>[];
|
||||||
|
|
||||||
|
_initialCenter = _points.isNotEmpty
|
||||||
|
? _points.first
|
||||||
|
: const LatLng(0, 0);
|
||||||
|
_initialZoom = _points.isNotEmpty ? 13.0 : 2.0;
|
||||||
|
_bounds = _points.length > 1 ? LatLngBounds.fromPoints(_points) : null;
|
||||||
|
_mapKey = ValueKey(
|
||||||
|
'${context.l10n.pathTrace_you},${_formatPathPrefixes(_traceData!.pathData)}',
|
||||||
|
);
|
||||||
|
_pathDistanceMeters = getPathDistanceMeters(_points);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
appLogger.error(
|
||||||
|
'Error handling trace response: $e',
|
||||||
|
tag: 'PathTraceMapScreen',
|
||||||
);
|
);
|
||||||
_pathDistanceMeters = getPathDistanceMeters(_points);
|
if (mounted) {
|
||||||
});
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_failed2Loaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<MeshCoreConnector>(
|
return Consumer<MeshCoreConnector>(
|
||||||
builder: (context, connector, _) {
|
builder: (context, connector, _) {
|
||||||
|
final settings = context.watch<AppSettingsService>().settings;
|
||||||
|
final isImperial = settings.unitSystem == UnitSystem.imperial;
|
||||||
final tileCache = context.read<MapTileCacheService>();
|
final tileCache = context.read<MapTileCacheService>();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -317,7 +438,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_hasData) _buildLegendCard(context, _traceData!),
|
if (_hasData)
|
||||||
|
_buildLegendCard(context, _traceData!, isImperial),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -326,55 +448,70 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Marker> _buildHopMarkers(List<int> pathData) {
|
List<Marker> _buildHopMarkers(
|
||||||
return [
|
List<int> pathData, {
|
||||||
for (final hop in pathData)
|
required bool showLabels,
|
||||||
if (_traceData!.pathContacts[hop] != null &&
|
}) {
|
||||||
_traceData!.pathContacts[hop]!.hasLocation)
|
final markers = <Marker>[];
|
||||||
Marker(
|
for (final hop in pathData) {
|
||||||
point: LatLng(
|
final contact = _traceData!.pathContacts[hop];
|
||||||
_traceData!.pathContacts[hop]!.latitude!,
|
final inferred = _inferredHopPositions[hop];
|
||||||
_traceData!.pathContacts[hop]!.longitude!,
|
final hasGps = contact != null && contact.hasLocation;
|
||||||
),
|
if (!hasGps && inferred == null) continue;
|
||||||
width: 35,
|
final point = hasGps
|
||||||
height: 35,
|
? LatLng(contact.latitude!, contact.longitude!)
|
||||||
child: Container(
|
: inferred!;
|
||||||
padding: const EdgeInsets.all(4),
|
final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||||
decoration: BoxDecoration(
|
markers.add(
|
||||||
color: Colors.green,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
border: Border.all(color: Colors.white, width: 2),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withValues(alpha: 0.3),
|
|
||||||
blurRadius: 4,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Text(
|
|
||||||
_traceData!.pathContacts[hop]!.publicKey
|
|
||||||
.sublist(0, 1)
|
|
||||||
.map(
|
|
||||||
(b) => b.toRadixString(16).padLeft(2, '0').toUpperCase(),
|
|
||||||
)
|
|
||||||
.join(),
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (context.read<MeshCoreConnector>().selfLatitude != null &&
|
|
||||||
context.read<MeshCoreConnector>().selfLongitude != null)
|
|
||||||
Marker(
|
Marker(
|
||||||
point: LatLng(
|
point: point,
|
||||||
context.read<MeshCoreConnector>().selfLatitude!,
|
width: 35,
|
||||||
context.read<MeshCoreConnector>().selfLongitude!,
|
height: 35,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: hasGps
|
||||||
|
? Colors.green
|
||||||
|
: Colors.orange.withValues(alpha: 0.75),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.3),
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
hasGps ? label : '~$label',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (showLabels) {
|
||||||
|
markers.add(
|
||||||
|
_buildNodeLabelMarker(
|
||||||
|
point: point,
|
||||||
|
label: contact?.name ?? '~$label',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final selfLat = context.read<MeshCoreConnector>().selfLatitude;
|
||||||
|
final selfLon = context.read<MeshCoreConnector>().selfLongitude;
|
||||||
|
if (selfLat != null && selfLon != null) {
|
||||||
|
final selfPoint = LatLng(selfLat, selfLon);
|
||||||
|
markers.add(
|
||||||
|
Marker(
|
||||||
|
point: selfPoint,
|
||||||
width: 35,
|
width: 35,
|
||||||
height: 35,
|
height: 35,
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -402,7 +539,94 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
);
|
||||||
|
if (showLabels) {
|
||||||
|
markers.add(
|
||||||
|
_buildNodeLabelMarker(
|
||||||
|
point: selfPoint,
|
||||||
|
label: context.l10n.pathTrace_you,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add target contact endpoint marker.
|
||||||
|
final targetPos = _targetContactPosition;
|
||||||
|
if (targetPos != null) {
|
||||||
|
final isGuessed = _targetContactIsGuessed;
|
||||||
|
final targetName = widget.targetContact?.name ?? '?';
|
||||||
|
markers.add(
|
||||||
|
Marker(
|
||||||
|
point: targetPos,
|
||||||
|
width: 35,
|
||||||
|
height: 35,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isGuessed
|
||||||
|
? Colors.purple.withValues(alpha: 0.55)
|
||||||
|
: Colors.red,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.3),
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: const Icon(Icons.person, color: Colors.white, size: 18),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (showLabels) {
|
||||||
|
markers.add(
|
||||||
|
_buildNodeLabelMarker(
|
||||||
|
point: targetPos,
|
||||||
|
label: isGuessed ? '~$targetName' : targetName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return markers;
|
||||||
|
}
|
||||||
|
|
||||||
|
Marker _buildNodeLabelMarker({required LatLng point, required String label}) {
|
||||||
|
return Marker(
|
||||||
|
point: point,
|
||||||
|
width: 120,
|
||||||
|
height: 24,
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: Transform.translate(
|
||||||
|
offset: const Offset(0, -20),
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black54,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String formatDirectionText(PathTraceData pathTraceData, int index) {
|
String formatDirectionText(PathTraceData pathTraceData, int index) {
|
||||||
@@ -482,6 +706,14 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
),
|
),
|
||||||
minZoom: 2.0,
|
minZoom: 2.0,
|
||||||
maxZoom: 18.0,
|
maxZoom: 18.0,
|
||||||
|
onPositionChanged: (camera, hasGesture) {
|
||||||
|
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
||||||
|
if (shouldShow != _showNodeLabels && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_showNodeLabels = shouldShow;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
TileLayer(
|
TileLayer(
|
||||||
@@ -492,12 +724,21 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
),
|
),
|
||||||
if (_polylines.isNotEmpty) PolylineLayer(polylines: _polylines),
|
if (_polylines.isNotEmpty) PolylineLayer(polylines: _polylines),
|
||||||
if (_traceData!.pathData.isNotEmpty)
|
if (_traceData!.pathData.isNotEmpty)
|
||||||
MarkerLayer(markers: _buildHopMarkers(_traceData!.pathData)),
|
MarkerLayer(
|
||||||
|
markers: _buildHopMarkers(
|
||||||
|
_traceData!.pathData,
|
||||||
|
showLabels: _showNodeLabels,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLegendCard(BuildContext context, PathTraceData pathTraceData) {
|
Widget _buildLegendCard(
|
||||||
|
BuildContext context,
|
||||||
|
PathTraceData pathTraceData,
|
||||||
|
bool isImperial,
|
||||||
|
) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
final maxHeight = MediaQuery.of(context).size.height * 0.35;
|
final maxHeight = MediaQuery.of(context).size.height * 0.35;
|
||||||
final estimatedHeight = 72.0 + (pathTraceData.pathData.length * 56.0);
|
final estimatedHeight = 72.0 + (pathTraceData.pathData.length * 56.0);
|
||||||
@@ -516,7 +757,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
child: Text(
|
child: Text(
|
||||||
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters)}',
|
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters, isImperial: isImperial)}',
|
||||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -532,6 +773,12 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
itemCount: pathTraceData.pathData.length + 1,
|
itemCount: pathTraceData.pathData.length + 1,
|
||||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
|
final snrUi = snrUiFromSNR(
|
||||||
|
index < pathTraceData.snrData.length
|
||||||
|
? pathTraceData.snrData[index]
|
||||||
|
: null,
|
||||||
|
context.read<MeshCoreConnector>().currentSf,
|
||||||
|
);
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -550,12 +797,22 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
),
|
),
|
||||||
style: const TextStyle(fontSize: 14),
|
style: const TextStyle(fontSize: 14),
|
||||||
),
|
),
|
||||||
trailing: SNRIcon(
|
trailing: Column(
|
||||||
snr:
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
pathTraceData.snrData[index].toSigned(
|
children: [
|
||||||
8,
|
Icon(
|
||||||
) /
|
snrUi.icon,
|
||||||
4.0,
|
color: snrUi.color,
|
||||||
|
size: 18.0,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
snrUi.text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: snrUi.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// Handle item tap
|
// Handle item tap
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
|||||||
|
|
||||||
_commandController.clear();
|
_commandController.clear();
|
||||||
_historyIndex = -1;
|
_historyIndex = -1;
|
||||||
|
_commandFocusNode.requestFocus();
|
||||||
|
|
||||||
// Auto-scroll to bottom
|
// Auto-scroll to bottom
|
||||||
Future.delayed(const Duration(milliseconds: 100), () {
|
Future.delayed(const Duration(milliseconds: 100), () {
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
|
import '../services/app_settings_service.dart';
|
||||||
import 'repeater_status_screen.dart';
|
import 'repeater_status_screen.dart';
|
||||||
import 'repeater_cli_screen.dart';
|
import 'repeater_cli_screen.dart';
|
||||||
import 'repeater_settings_screen.dart';
|
import 'repeater_settings_screen.dart';
|
||||||
import 'telemetry_screen.dart';
|
import 'telemetry_screen.dart';
|
||||||
import 'neighbours_screen.dart';
|
import 'neighbors_screen.dart';
|
||||||
|
|
||||||
class RepeaterHubScreen extends StatelessWidget {
|
class RepeaterHubScreen extends StatelessWidget {
|
||||||
final Contact repeater;
|
final Contact repeater;
|
||||||
@@ -21,6 +23,10 @@ class RepeaterHubScreen extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
|
final settingsService = context.watch<AppSettingsService>();
|
||||||
|
final chemistry = settingsService.batteryChemistryForRepeater(
|
||||||
|
repeater.publicKeyHex,
|
||||||
|
);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Column(
|
title: Column(
|
||||||
@@ -107,6 +113,62 @@ class RepeaterHubScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.battery_full),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
l10n.appSettings_batteryChemistry,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
initialValue: chemistry,
|
||||||
|
isExpanded: true,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
border: UnderlineInputBorder(),
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == null) return;
|
||||||
|
settingsService.setBatteryChemistryForRepeater(
|
||||||
|
repeater.publicKeyHex,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'nmc',
|
||||||
|
child: Text(l10n.appSettings_batteryNmc),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'lifepo4',
|
||||||
|
child: Text(l10n.appSettings_batteryLifepo4),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'lipo',
|
||||||
|
child: Text(l10n.appSettings_batteryLipo),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
Text(
|
Text(
|
||||||
l10n.repeater_managementTools,
|
l10n.repeater_managementTools,
|
||||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
@@ -174,17 +236,15 @@ class RepeaterHubScreen extends StatelessWidget {
|
|||||||
_buildManagementCard(
|
_buildManagementCard(
|
||||||
context,
|
context,
|
||||||
icon: Icons.group,
|
icon: Icons.group,
|
||||||
title: l10n.repeater_neighbours,
|
title: l10n.repeater_neighbors,
|
||||||
subtitle: l10n.repeater_neighboursSubtitle,
|
subtitle: l10n.repeater_neighborsSubtitle,
|
||||||
color: Colors.orange,
|
color: Colors.orange,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => NeighboursScreen(
|
builder: (context) =>
|
||||||
repeater: repeater,
|
NeighborsScreen(repeater: repeater, password: password),
|
||||||
password: password,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import '../models/contact.dart';
|
|||||||
import '../models/path_selection.dart';
|
import '../models/path_selection.dart';
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
|
import '../services/app_settings_service.dart';
|
||||||
import '../services/repeater_command_service.dart';
|
import '../services/repeater_command_service.dart';
|
||||||
|
import '../utils/battery_utils.dart';
|
||||||
import '../widgets/path_management_dialog.dart';
|
import '../widgets/path_management_dialog.dart';
|
||||||
|
|
||||||
class RepeaterStatusScreen extends StatefulWidget {
|
class RepeaterStatusScreen extends StatefulWidget {
|
||||||
@@ -179,6 +181,12 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
|||||||
_dupDirect = directDups;
|
_dupDirect = directDups;
|
||||||
_dupFlood = floodDups;
|
_dupFlood = floodDups;
|
||||||
});
|
});
|
||||||
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
|
connector.updateRepeaterBatterySnapshot(
|
||||||
|
widget.repeater.publicKeyHex,
|
||||||
|
batteryMv,
|
||||||
|
source: 'status_binary',
|
||||||
|
);
|
||||||
_recordStatusResult(true);
|
_recordStatusResult(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,6 +209,18 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
|||||||
_uptimeSecs = _asInt(data['uptime_secs']);
|
_uptimeSecs = _asInt(data['uptime_secs']);
|
||||||
_queueLen = _asInt(data['queue_len']);
|
_queueLen = _asInt(data['queue_len']);
|
||||||
_debugFlags = _asInt(data['errors']);
|
_debugFlags = _asInt(data['errors']);
|
||||||
|
final batteryMv = _batteryMv;
|
||||||
|
if (batteryMv != null) {
|
||||||
|
final connector = Provider.of<MeshCoreConnector>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
);
|
||||||
|
connector.updateRepeaterBatterySnapshot(
|
||||||
|
widget.repeater.publicKeyHex,
|
||||||
|
batteryMv,
|
||||||
|
source: 'status_text',
|
||||||
|
);
|
||||||
|
}
|
||||||
} else if (data.containsKey('noise_floor')) {
|
} else if (data.containsKey('noise_floor')) {
|
||||||
_noiseFloor = _asInt(data['noise_floor']);
|
_noiseFloor = _asInt(data['noise_floor']);
|
||||||
_lastRssi = _asInt(data['last_rssi']);
|
_lastRssi = _asInt(data['last_rssi']);
|
||||||
@@ -590,18 +610,24 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _batteryText() {
|
String _batteryText() {
|
||||||
if (_batteryMv == null) return '—';
|
final connector = context.watch<MeshCoreConnector>();
|
||||||
final percent = _batteryPercentFromMv(_batteryMv!);
|
final batteryMv =
|
||||||
final volts = (_batteryMv! / 1000.0).toStringAsFixed(2);
|
connector.getRepeaterBatteryMillivolts(widget.repeater.publicKeyHex) ??
|
||||||
|
_batteryMv;
|
||||||
|
if (batteryMv == null) return '—';
|
||||||
|
final percent = estimateBatteryPercentFromMillivolts(
|
||||||
|
batteryMv,
|
||||||
|
_batteryChemistry(),
|
||||||
|
);
|
||||||
|
final volts = (batteryMv / 1000.0).toStringAsFixed(2);
|
||||||
return '$percent% / ${volts}V';
|
return '$percent% / ${volts}V';
|
||||||
}
|
}
|
||||||
|
|
||||||
int _batteryPercentFromMv(int millivolts) {
|
String _batteryChemistry() {
|
||||||
const minMv = 3000;
|
final settingsService = context.read<AppSettingsService>();
|
||||||
const maxMv = 4200;
|
return settingsService.batteryChemistryForRepeater(
|
||||||
if (millivolts <= minMv) return 0;
|
widget.repeater.publicKeyHex,
|
||||||
if (millivolts >= maxMv) return 100;
|
);
|
||||||
return (((millivolts - minMv) * 100) / (maxMv - minMv)).round();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String _clockText() {
|
String _clockText() {
|
||||||
|
|||||||
+101
-45
@@ -1,14 +1,16 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io' show Platform;
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import '../utils/platform_info.dart';
|
||||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
|
import '../utils/app_logger.dart';
|
||||||
|
import '../widgets/adaptive_app_bar_title.dart';
|
||||||
import '../widgets/device_tile.dart';
|
import '../widgets/device_tile.dart';
|
||||||
import 'contacts_screen.dart';
|
import 'contacts_screen.dart';
|
||||||
|
import 'usb_screen.dart';
|
||||||
|
|
||||||
/// Screen for scanning and connecting to MeshCore devices
|
/// Screen for scanning and connecting to MeshCore devices
|
||||||
class ScannerScreen extends StatefulWidget {
|
class ScannerScreen extends StatefulWidget {
|
||||||
@@ -20,6 +22,7 @@ class ScannerScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _ScannerScreenState extends State<ScannerScreen> {
|
class _ScannerScreenState extends State<ScannerScreen> {
|
||||||
bool _changedNavigation = false;
|
bool _changedNavigation = false;
|
||||||
|
late final MeshCoreConnector _connector;
|
||||||
late final VoidCallback _connectionListener;
|
late final VoidCallback _connectionListener;
|
||||||
BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown;
|
BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown;
|
||||||
late StreamSubscription<BluetoothAdapterState> _bluetoothStateSubscription;
|
late StreamSubscription<BluetoothAdapterState> _bluetoothStateSubscription;
|
||||||
@@ -27,12 +30,15 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
_connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
|
|
||||||
_connectionListener = () {
|
_connectionListener = () {
|
||||||
if (connector.state == MeshCoreConnectionState.disconnected) {
|
final isCurrentRoute = ModalRoute.of(context)?.isCurrent ?? true;
|
||||||
|
if (_connector.state == MeshCoreConnectionState.disconnected) {
|
||||||
_changedNavigation = false;
|
_changedNavigation = false;
|
||||||
} else if (connector.state == MeshCoreConnectionState.connected &&
|
} else if (_connector.state == MeshCoreConnectionState.connected &&
|
||||||
|
_connector.activeTransport == MeshCoreTransportType.bluetooth &&
|
||||||
|
isCurrentRoute &&
|
||||||
!_changedNavigation) {
|
!_changedNavigation) {
|
||||||
_changedNavigation = true;
|
_changedNavigation = true;
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -43,34 +49,53 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
connector.addListener(_connectionListener);
|
_connector.addListener(_connectionListener);
|
||||||
|
|
||||||
_bluetoothStateSubscription = FlutterBluePlus.adapterState.listen((state) {
|
_bluetoothStateSubscription = FlutterBluePlus.adapterState.listen(
|
||||||
if (mounted) {
|
(state) {
|
||||||
setState(() {
|
if (mounted) {
|
||||||
_bluetoothState = state;
|
setState(() {
|
||||||
});
|
_bluetoothState = state;
|
||||||
// Cancel scan if Bluetooth turns off while scanning
|
});
|
||||||
if (state != BluetoothAdapterState.on) {
|
// Cancel scan if Bluetooth turns off while scanning
|
||||||
unawaited(connector.stopScan());
|
if (state != BluetoothAdapterState.on) {
|
||||||
|
unawaited(_connector.stopScan());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
onError: (Object e) {
|
||||||
|
appLogger.warn('Adapter state stream error: $e', tag: 'ScannerScreen');
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
_connector.removeListener(_connectionListener);
|
||||||
connector.removeListener(_connectionListener);
|
|
||||||
unawaited(_bluetoothStateSubscription.cancel());
|
unawaited(_bluetoothStateSubscription.cancel());
|
||||||
|
if (!_changedNavigation) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
unawaited(_connector.disconnect(manual: true));
|
||||||
|
});
|
||||||
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final canPop = Navigator.of(context).canPop();
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(context.l10n.scanner_title),
|
leading: canPop
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () {
|
||||||
|
appLogger.info('Back button pressed', tag: 'ScannerScreen');
|
||||||
|
Navigator.of(context).maybePop();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
title: AdaptiveAppBarTitle(context.l10n.scanner_title),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
),
|
),
|
||||||
@@ -94,36 +119,67 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
floatingActionButton: Consumer<MeshCoreConnector>(
|
bottomNavigationBar: Consumer<MeshCoreConnector>(
|
||||||
builder: (context, connector, child) {
|
builder: (context, connector, child) {
|
||||||
final isScanning =
|
final isScanning =
|
||||||
connector.state == MeshCoreConnectionState.scanning;
|
connector.state == MeshCoreConnectionState.scanning;
|
||||||
final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off;
|
final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off;
|
||||||
|
final usbSupported = PlatformInfo.supportsUsbSerial;
|
||||||
|
|
||||||
return FloatingActionButton.extended(
|
return SafeArea(
|
||||||
onPressed: isBluetoothOff
|
top: false,
|
||||||
? null
|
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||||
: () {
|
child: Row(
|
||||||
if (isScanning) {
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
connector.stopScan();
|
children: [
|
||||||
} else {
|
if (usbSupported)
|
||||||
connector.startScan();
|
FloatingActionButton.extended(
|
||||||
}
|
onPressed: () {
|
||||||
},
|
appLogger.info(
|
||||||
icon: isScanning
|
'USB selected, opening UsbScreen',
|
||||||
? const SizedBox(
|
tag: 'ScannerScreen',
|
||||||
width: 20,
|
);
|
||||||
height: 20,
|
Navigator.of(context).push(
|
||||||
child: CircularProgressIndicator(
|
MaterialPageRoute(builder: (_) => const UsbScreen()),
|
||||||
strokeWidth: 2,
|
);
|
||||||
color: Colors.white,
|
},
|
||||||
),
|
heroTag: 'scanner_usb_action',
|
||||||
)
|
icon: const Icon(Icons.usb),
|
||||||
: const Icon(Icons.bluetooth_searching),
|
label: Text(context.l10n.connectionChoiceUsbLabel),
|
||||||
label: Text(
|
),
|
||||||
isScanning
|
if (usbSupported) const SizedBox(width: 12),
|
||||||
? context.l10n.scanner_stop
|
FloatingActionButton.extended(
|
||||||
: context.l10n.scanner_scan,
|
heroTag: 'scanner_ble_action',
|
||||||
|
onPressed: isBluetoothOff
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
if (isScanning) {
|
||||||
|
connector.stopScan();
|
||||||
|
} else {
|
||||||
|
unawaited(
|
||||||
|
connector.startScan().catchError((e) {
|
||||||
|
appLogger.warn(
|
||||||
|
'startScan error: $e',
|
||||||
|
tag: 'ScannerScreen',
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: isScanning
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.bluetooth_searching),
|
||||||
|
label: Text(
|
||||||
|
isScanning
|
||||||
|
? context.l10n.scanner_stop
|
||||||
|
: context.l10n.scanner_scan,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -264,7 +320,7 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (Platform.isAndroid)
|
if (PlatformInfo.isAndroid)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => FlutterBluePlus.turnOn(),
|
onPressed: () => FlutterBluePlus.turnOn(),
|
||||||
child: Text(context.l10n.scanner_enableBluetooth),
|
child: Text(context.l10n.scanner_enableBluetooth),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import '../connector/meshcore_connector.dart';
|
|||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../models/radio_settings.dart';
|
import '../models/radio_settings.dart';
|
||||||
|
import '../widgets/app_bar.dart';
|
||||||
import 'app_settings_screen.dart';
|
import 'app_settings_screen.dart';
|
||||||
import 'app_debug_log_screen.dart';
|
import 'app_debug_log_screen.dart';
|
||||||
import 'ble_debug_log_screen.dart';
|
import 'ble_debug_log_screen.dart';
|
||||||
@@ -41,7 +42,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: Text(l10n.settings_title), centerTitle: true),
|
appBar: AppBar(
|
||||||
|
title: AppBarTitle(
|
||||||
|
l10n.settings_title,
|
||||||
|
indicators: false,
|
||||||
|
subtitle: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
child: Consumer<MeshCoreConnector>(
|
child: Consumer<MeshCoreConnector>(
|
||||||
@@ -270,6 +277,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
onTap: () => _editLocation(context, connector),
|
onTap: () => _editLocation(context, connector),
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.group_add_outlined),
|
||||||
|
title: Text(l10n.settings_contactSettings),
|
||||||
|
subtitle: Text(l10n.settings_contactSettingsSubtitle),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () => _editAutoAddConfig(context, connector),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.visibility_off_outlined),
|
leading: const Icon(Icons.visibility_off_outlined),
|
||||||
title: Text(l10n.settings_privacyMode),
|
title: Text(l10n.settings_privacyMode),
|
||||||
@@ -845,6 +860,121 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _editAutoAddConfig(BuildContext context, MeshCoreConnector connector) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
bool autoAddChat = false;
|
||||||
|
bool autoAddRepeater = false;
|
||||||
|
bool autoAddRoomServer = false;
|
||||||
|
bool autoAddSensor = false;
|
||||||
|
bool overwriteOldest = false;
|
||||||
|
|
||||||
|
final connector = context.read<MeshCoreConnector>();
|
||||||
|
autoAddChat = connector.autoAddUsers ?? false;
|
||||||
|
autoAddRepeater = connector.autoAddRepeaters ?? false;
|
||||||
|
autoAddRoomServer = connector.autoAddRoomServers ?? false;
|
||||||
|
autoAddSensor = connector.autoAddSensors ?? false;
|
||||||
|
overwriteOldest = connector.autoAddOverwriteOldest ?? false;
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) => StatefulBuilder(
|
||||||
|
builder: (context, setDialogState) => AlertDialog(
|
||||||
|
title: Text(l10n.contactsSettings_autoAddTitle),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
FeatureToggleRow(
|
||||||
|
title: l10n.contactsSettings_autoAddUsersTitle,
|
||||||
|
subtitle: l10n.contactsSettings_autoAddUsersSubtitle,
|
||||||
|
value: autoAddChat,
|
||||||
|
onChanged: (value) {
|
||||||
|
setDialogState(() => autoAddChat = value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
FeatureToggleRow(
|
||||||
|
title: l10n.contactsSettings_autoAddRepeatersTitle,
|
||||||
|
subtitle: l10n.contactsSettings_autoAddRepeatersSubtitle,
|
||||||
|
value: autoAddRepeater,
|
||||||
|
onChanged: (value) {
|
||||||
|
setDialogState(() => autoAddRepeater = value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
FeatureToggleRow(
|
||||||
|
title: l10n.contactsSettings_autoAddRoomServersTitle,
|
||||||
|
subtitle: l10n.contactsSettings_autoAddRoomServersSubtitle,
|
||||||
|
value: autoAddRoomServer,
|
||||||
|
onChanged: (value) {
|
||||||
|
setDialogState(() => autoAddRoomServer = value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
FeatureToggleRow(
|
||||||
|
title: l10n.contactsSettings_autoAddSensorsTitle,
|
||||||
|
subtitle: l10n.contactsSettings_autoAddSensorsSubtitle,
|
||||||
|
value: autoAddSensor,
|
||||||
|
onChanged: (value) {
|
||||||
|
setDialogState(() => autoAddSensor = value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Divider(height: 4),
|
||||||
|
FeatureToggleRow(
|
||||||
|
title: l10n.contactsSettings_overwriteOldestTitle,
|
||||||
|
subtitle: l10n.contactsSettings_overwriteOldestSubtitle,
|
||||||
|
value: overwriteOldest,
|
||||||
|
onChanged: (value) {
|
||||||
|
setDialogState(() => overwriteOldest = value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(l10n.common_cancel),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
_sendSettings(
|
||||||
|
connector,
|
||||||
|
autoAddChat,
|
||||||
|
autoAddRepeater,
|
||||||
|
autoAddRoomServer,
|
||||||
|
autoAddSensor,
|
||||||
|
overwriteOldest,
|
||||||
|
);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Text(l10n.common_save),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _sendSettings(
|
||||||
|
MeshCoreConnector connector,
|
||||||
|
bool autoAddChat,
|
||||||
|
bool autoAddRepeater,
|
||||||
|
bool autoAddRoomServer,
|
||||||
|
bool autoAddSensor,
|
||||||
|
bool overwriteOldest,
|
||||||
|
) async {
|
||||||
|
final frame = buildSetAutoAddConfigFrame(
|
||||||
|
autoAddChat: autoAddChat,
|
||||||
|
autoAddRepeater: autoAddRepeater,
|
||||||
|
autoAddRoomServer: autoAddRoomServer,
|
||||||
|
autoAddSensor: autoAddSensor,
|
||||||
|
overwriteOldest: overwriteOldest,
|
||||||
|
);
|
||||||
|
await connector.sendFrame(frame);
|
||||||
|
await connector.sendFrame(buildGetAutoAddFlagsFrame());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RadioSettingsDialog extends StatefulWidget {
|
class _RadioSettingsDialog extends StatefulWidget {
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ import 'package:provider/provider.dart';
|
|||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
import '../models/path_selection.dart';
|
import '../models/path_selection.dart';
|
||||||
|
import '../models/app_settings.dart';
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
|
import '../services/app_settings_service.dart';
|
||||||
import '../services/repeater_command_service.dart';
|
import '../services/repeater_command_service.dart';
|
||||||
import '../widgets/path_management_dialog.dart';
|
import '../widgets/path_management_dialog.dart';
|
||||||
import '../helpers/cayenne_lpp.dart';
|
import '../helpers/cayenne_lpp.dart';
|
||||||
|
import '../utils/battery_utils.dart';
|
||||||
|
|
||||||
class TelemetryScreen extends StatefulWidget {
|
class TelemetryScreen extends StatefulWidget {
|
||||||
final Contact repeater;
|
final Contact repeater;
|
||||||
@@ -72,9 +75,19 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleStatusResponse(Uint8List frame) {
|
void _handleStatusResponse(Uint8List frame) {
|
||||||
|
final parsedTelemetry = CayenneLpp.parseByChannel(frame);
|
||||||
|
final batteryMv = _extractTelemetryBatteryMillivolts(parsedTelemetry);
|
||||||
|
if (batteryMv != null) {
|
||||||
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
|
connector.updateRepeaterBatterySnapshot(
|
||||||
|
widget.repeater.publicKeyHex,
|
||||||
|
batteryMv,
|
||||||
|
source: 'telemetry',
|
||||||
|
);
|
||||||
|
}
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_parsedTelemetry = CayenneLpp.parseByChannel(frame);
|
_parsedTelemetry = parsedTelemetry;
|
||||||
});
|
});
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -181,6 +194,8 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
final connector = context.watch<MeshCoreConnector>();
|
final connector = context.watch<MeshCoreConnector>();
|
||||||
|
final settings = context.watch<AppSettingsService>().settings;
|
||||||
|
final isImperialUnits = settings.unitSystem == UnitSystem.imperial;
|
||||||
final repeater = _resolveRepeater(connector);
|
final repeater = _resolveRepeater(connector);
|
||||||
final isFloodMode = repeater.pathOverride == -1;
|
final isFloodMode = repeater.pathOverride == -1;
|
||||||
|
|
||||||
@@ -307,6 +322,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||||||
entry['values'],
|
entry['values'],
|
||||||
l10n.telemetry_channelTitle(entry['channel']),
|
l10n.telemetry_channelTitle(entry['channel']),
|
||||||
entry['channel'],
|
entry['channel'],
|
||||||
|
isImperialUnits,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -319,6 +335,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||||||
Map<String, dynamic> channelData,
|
Map<String, dynamic> channelData,
|
||||||
String title,
|
String title,
|
||||||
int channel,
|
int channel,
|
||||||
|
bool isImperialUnits,
|
||||||
) {
|
) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
return Card(
|
return Card(
|
||||||
@@ -358,12 +375,12 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||||||
else if (entry.key == 'temperature' && channel == 1)
|
else if (entry.key == 'temperature' && channel == 1)
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
l10n.telemetry_mcuTemperatureLabel,
|
l10n.telemetry_mcuTemperatureLabel,
|
||||||
_temperatureText(entry.value),
|
_temperatureText(entry.value, isImperialUnits),
|
||||||
)
|
)
|
||||||
else if (entry.key == 'temperature')
|
else if (entry.key == 'temperature')
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
l10n.telemetry_temperatureLabel,
|
l10n.telemetry_temperatureLabel,
|
||||||
_temperatureText(entry.value),
|
_temperatureText(entry.value, isImperialUnits),
|
||||||
)
|
)
|
||||||
else if (entry.key == 'current' && channel == 1)
|
else if (entry.key == 'current' && channel == 1)
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
@@ -405,29 +422,44 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _batteryText(double? batteryMv) {
|
int? _extractTelemetryBatteryMillivolts(List<Map<String, dynamic>> entries) {
|
||||||
|
for (final entry in entries) {
|
||||||
|
if (entry['channel'] != 1) continue;
|
||||||
|
final values = entry['values'];
|
||||||
|
if (values is! Map<String, dynamic>) continue;
|
||||||
|
final voltage = values['voltage'];
|
||||||
|
if (voltage is num) return (voltage.toDouble() * 1000).round();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _batteryText(double? telemetryVolts) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
|
final connector = context.watch<MeshCoreConnector>();
|
||||||
|
final batteryMv =
|
||||||
|
connector.getRepeaterBatteryMillivolts(widget.repeater.publicKeyHex) ??
|
||||||
|
(telemetryVolts == null ? null : (telemetryVolts * 1000).round());
|
||||||
if (batteryMv == null) return l10n.common_notAvailable;
|
if (batteryMv == null) return l10n.common_notAvailable;
|
||||||
final percent = _batteryPercentFromMv(batteryMv);
|
final chemistry = _batteryChemistry();
|
||||||
final volts = batteryMv.toStringAsFixed(2);
|
final percent = estimateBatteryPercentFromMillivolts(batteryMv, chemistry);
|
||||||
|
final volts = (batteryMv / 1000).toStringAsFixed(2);
|
||||||
return l10n.telemetry_batteryValue(percent, volts);
|
return l10n.telemetry_batteryValue(percent, volts);
|
||||||
}
|
}
|
||||||
|
|
||||||
int _batteryPercentFromMv(double millivolts) {
|
String _batteryChemistry() {
|
||||||
const minMv = 2.800;
|
final settingsService = context.read<AppSettingsService>();
|
||||||
const maxMv = 4.200;
|
return settingsService.batteryChemistryForRepeater(
|
||||||
if (millivolts <= minMv) return 0;
|
widget.repeater.publicKeyHex,
|
||||||
if (millivolts >= maxMv) return 100;
|
);
|
||||||
return (((millivolts - minMv) * 100) / (maxMv - minMv)).round();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String _temperatureText(double? tempC) {
|
String _temperatureText(double? tempC, bool isImperialUnits) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
if (tempC == null) return l10n.common_notAvailable;
|
if (tempC == null) return l10n.common_notAvailable;
|
||||||
final tempF = (tempC * 9 / 5) + 32;
|
final tempF = (tempC * 9 / 5) + 32;
|
||||||
return l10n.telemetry_temperatureValue(
|
if (isImperialUnits) {
|
||||||
tempC.toStringAsFixed(1),
|
return '${tempF.toStringAsFixed(1)}°F';
|
||||||
tempF.toStringAsFixed(1),
|
}
|
||||||
);
|
return '${tempC.toStringAsFixed(1)}°C';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,405 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../connector/meshcore_connector.dart';
|
||||||
|
import '../l10n/l10n.dart';
|
||||||
|
import '../utils/app_logger.dart';
|
||||||
|
import '../utils/platform_info.dart';
|
||||||
|
import '../utils/usb_port_labels.dart';
|
||||||
|
import '../widgets/adaptive_app_bar_title.dart';
|
||||||
|
import 'contacts_screen.dart';
|
||||||
|
import 'scanner_screen.dart';
|
||||||
|
|
||||||
|
class UsbScreen extends StatefulWidget {
|
||||||
|
const UsbScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<UsbScreen> createState() => _UsbScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _UsbScreenState extends State<UsbScreen> {
|
||||||
|
final List<String> _ports = <String>[];
|
||||||
|
bool _isLoadingPorts = true;
|
||||||
|
bool _navigatedToContacts = false;
|
||||||
|
bool _didScheduleInitialLoad = false;
|
||||||
|
Timer? _hotPlugTimer;
|
||||||
|
late final MeshCoreConnector _connector;
|
||||||
|
late final VoidCallback _connectionListener;
|
||||||
|
|
||||||
|
bool get _supportsHotPlug =>
|
||||||
|
PlatformInfo.isWindows || PlatformInfo.isLinux || PlatformInfo.isMacOS;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_connector = context.read<MeshCoreConnector>();
|
||||||
|
_connectionListener = () {
|
||||||
|
if (!mounted) return;
|
||||||
|
if (_connector.state == MeshCoreConnectionState.disconnected) {
|
||||||
|
_navigatedToContacts = false;
|
||||||
|
}
|
||||||
|
if (_connector.state == MeshCoreConnectionState.connected &&
|
||||||
|
_connector.isUsbTransportConnected &&
|
||||||
|
!_navigatedToContacts) {
|
||||||
|
_navigatedToContacts = true;
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(builder: (_) => const ContactsScreen()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_connector.addListener(_connectionListener);
|
||||||
|
_startHotPlugTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
_connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus);
|
||||||
|
_connector.setUsbFallbackDeviceName(context.l10n.usbFallbackDeviceName);
|
||||||
|
if (!_didScheduleInitialLoad) {
|
||||||
|
_didScheduleInitialLoad = true;
|
||||||
|
unawaited(_loadPorts());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_hotPlugTimer?.cancel();
|
||||||
|
_hotPlugTimer = null;
|
||||||
|
_connector.removeListener(_connectionListener);
|
||||||
|
if (!_navigatedToContacts &&
|
||||||
|
_connector.activeTransport == MeshCoreTransportType.usb &&
|
||||||
|
_connector.state != MeshCoreConnectionState.disconnected) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
unawaited(_connector.disconnect(manual: true));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => Navigator.of(context).maybePop(),
|
||||||
|
),
|
||||||
|
title: AdaptiveAppBarTitle(context.l10n.usbScreenTitle),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: Consumer<MeshCoreConnector>(
|
||||||
|
builder: (context, connector, child) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_buildStatusBar(context, connector),
|
||||||
|
Expanded(child: _buildPortList(context, connector)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bottomNavigationBar: Consumer<MeshCoreConnector>(
|
||||||
|
builder: (context, connector, child) {
|
||||||
|
final isLoading = _isLoadingPorts;
|
||||||
|
final showBle = PlatformInfo.isWeb ||
|
||||||
|
PlatformInfo.isAndroid ||
|
||||||
|
PlatformInfo.isIOS;
|
||||||
|
|
||||||
|
return SafeArea(
|
||||||
|
top: false,
|
||||||
|
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
if (showBle)
|
||||||
|
FloatingActionButton.extended(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => const ScannerScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
heroTag: 'usb_ble_action',
|
||||||
|
icon: const Icon(Icons.bluetooth),
|
||||||
|
label: Text(context.l10n.connectionChoiceBluetoothLabel),
|
||||||
|
),
|
||||||
|
if (showBle) const SizedBox(width: 12),
|
||||||
|
if (!_supportsHotPlug)
|
||||||
|
FloatingActionButton.extended(
|
||||||
|
onPressed: isLoading ? null : _loadPorts,
|
||||||
|
heroTag: 'usb_refresh_action',
|
||||||
|
icon: isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.refresh),
|
||||||
|
label: Text(context.l10n.repeater_refresh),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatusBar(BuildContext context, MeshCoreConnector connector) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
String statusText;
|
||||||
|
Color statusColor;
|
||||||
|
|
||||||
|
if (_isLoadingPorts) {
|
||||||
|
statusText = l10n.usbStatus_searching;
|
||||||
|
statusColor = Colors.blue;
|
||||||
|
} else if (connector.isUsbTransportConnected) {
|
||||||
|
switch (connector.state) {
|
||||||
|
case MeshCoreConnectionState.connected:
|
||||||
|
statusText = l10n.scanner_connectedTo(
|
||||||
|
connector.activeUsbPortDisplayLabel ?? 'USB',
|
||||||
|
);
|
||||||
|
statusColor = Colors.green;
|
||||||
|
case MeshCoreConnectionState.disconnecting:
|
||||||
|
statusText = l10n.scanner_disconnecting;
|
||||||
|
statusColor = Colors.orange;
|
||||||
|
default:
|
||||||
|
statusText = l10n.usbStatus_notConnected;
|
||||||
|
statusColor = Colors.grey;
|
||||||
|
}
|
||||||
|
} else if (connector.state == MeshCoreConnectionState.connecting &&
|
||||||
|
connector.activeTransport == MeshCoreTransportType.usb) {
|
||||||
|
statusText = l10n.usbStatus_connecting;
|
||||||
|
statusColor = Colors.orange;
|
||||||
|
} else {
|
||||||
|
statusText = l10n.usbStatus_notConnected;
|
||||||
|
statusColor = Colors.grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||||
|
color: statusColor.withValues(alpha: 0.1),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.circle, size: 12, color: statusColor),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
statusText,
|
||||||
|
style: TextStyle(color: statusColor, fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPortList(BuildContext context, MeshCoreConnector connector) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
|
||||||
|
if (_isLoadingPorts) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.usb, size: 64, color: Colors.grey[400]),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
l10n.usbStatus_searching,
|
||||||
|
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_ports.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.usb, size: 64, color: Colors.grey[400]),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
l10n.usbScreenEmptyState,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final isConnecting =
|
||||||
|
connector.state == MeshCoreConnectionState.connecting &&
|
||||||
|
connector.activeTransport == MeshCoreTransportType.usb;
|
||||||
|
|
||||||
|
return ListView.separated(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
itemCount: _ports.length,
|
||||||
|
separatorBuilder: (context, index) => const Divider(),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final port = _ports[index];
|
||||||
|
final displayName = friendlyUsbPortName(port);
|
||||||
|
final rawName = normalizeUsbPortName(port);
|
||||||
|
final showRawName =
|
||||||
|
rawName != displayName && !rawName.startsWith('web:');
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.usb),
|
||||||
|
title: Text(
|
||||||
|
displayName,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
subtitle: showRawName ? Text(rawName) : null,
|
||||||
|
trailing: ElevatedButton(
|
||||||
|
onPressed:
|
||||||
|
isConnecting ? null : () => _connectPort(port),
|
||||||
|
child: Text(l10n.common_connect),
|
||||||
|
),
|
||||||
|
onTap: isConnecting ? null : () => _connectPort(port),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startHotPlugTimer() {
|
||||||
|
if (!_supportsHotPlug) return;
|
||||||
|
_hotPlugTimer?.cancel();
|
||||||
|
_hotPlugTimer = Timer.periodic(const Duration(seconds: 2), (_) {
|
||||||
|
_pollHotPlug();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pollHotPlug() async {
|
||||||
|
if (_isLoadingPorts) return;
|
||||||
|
if (!mounted) return;
|
||||||
|
// Don't poll while connecting or connected.
|
||||||
|
if (_connector.state != MeshCoreConnectionState.disconnected) return;
|
||||||
|
try {
|
||||||
|
final ports = await _connector.listUsbPorts();
|
||||||
|
if (!mounted) return;
|
||||||
|
final added = ports.where((p) => !_ports.contains(p)).toList();
|
||||||
|
final removed = _ports.where((p) => !ports.contains(p)).toList();
|
||||||
|
if (added.isEmpty && removed.isEmpty) return;
|
||||||
|
setState(() {
|
||||||
|
_ports
|
||||||
|
..clear()
|
||||||
|
..addAll(ports);
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
// Silent — hot-plug failures are non-critical.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadPorts() async {
|
||||||
|
if (!mounted) return;
|
||||||
|
_connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoadingPorts = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final ports = await _connector.listUsbPorts();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_ports
|
||||||
|
..clear()
|
||||||
|
..addAll(ports);
|
||||||
|
_isLoadingPorts = false;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_ports.clear();
|
||||||
|
_isLoadingPorts = false;
|
||||||
|
});
|
||||||
|
_showError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _connectPort(String port) async {
|
||||||
|
if (_connector.state != MeshCoreConnectionState.disconnected) return;
|
||||||
|
|
||||||
|
final rawPortName = normalizeUsbPortName(port);
|
||||||
|
appLogger.info('Connect tapped for $port (raw: $rawPortName)',
|
||||||
|
tag: 'UsbScreen');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _connector.connectUsb(portName: rawPortName);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
appLogger.error(
|
||||||
|
'Connect failed for $rawPortName: $error\n$stackTrace',
|
||||||
|
tag: 'UsbScreen',
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
_showError(error);
|
||||||
|
unawaited(_loadPorts());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showError(Object error) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(_friendlyErrorMessage(error)),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _friendlyErrorMessage(Object error) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
|
||||||
|
if (error is PlatformException) {
|
||||||
|
switch (error.code) {
|
||||||
|
case 'usb_permission_denied':
|
||||||
|
return l10n.usbErrorPermissionDenied;
|
||||||
|
case 'usb_device_missing':
|
||||||
|
case 'usb_device_detached':
|
||||||
|
return l10n.usbErrorDeviceMissing;
|
||||||
|
case 'usb_invalid_port':
|
||||||
|
return l10n.usbErrorInvalidPort;
|
||||||
|
case 'usb_busy':
|
||||||
|
return l10n.usbErrorBusy;
|
||||||
|
case 'usb_not_connected':
|
||||||
|
return l10n.usbErrorNotConnected;
|
||||||
|
case 'usb_open_failed':
|
||||||
|
case 'usb_driver_missing':
|
||||||
|
return l10n.usbErrorOpenFailed;
|
||||||
|
case 'usb_connect_failed':
|
||||||
|
return l10n.usbErrorConnectFailed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error is UnsupportedError) {
|
||||||
|
return l10n.usbErrorUnsupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error is StateError) {
|
||||||
|
final msg = error.message;
|
||||||
|
if (msg.contains('already active')) return l10n.usbErrorAlreadyActive;
|
||||||
|
if (msg.contains('No USB serial device selected')) {
|
||||||
|
return l10n.usbErrorNoDeviceSelected;
|
||||||
|
}
|
||||||
|
if (msg.contains('not open') || msg.contains('closed')) {
|
||||||
|
return l10n.usbErrorPortClosed;
|
||||||
|
}
|
||||||
|
if (msg.contains('Timed out')) return l10n.usbErrorConnectTimedOut;
|
||||||
|
if (msg.contains('Failed to open')) return l10n.usbErrorOpenFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error is TimeoutException) {
|
||||||
|
return l10n.usbErrorConnectTimedOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
return error.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,7 +52,12 @@ class AppDebugLogService extends ChangeNotifier {
|
|||||||
String tag = 'App',
|
String tag = 'App',
|
||||||
AppDebugLogLevel level = AppDebugLogLevel.info,
|
AppDebugLogLevel level = AppDebugLogLevel.info,
|
||||||
}) {
|
}) {
|
||||||
if (!_enabled) return;
|
if (!_enabled && !kDebugMode) return;
|
||||||
|
if (!_enabled) {
|
||||||
|
// In debug mode, still print to console but don't store entries.
|
||||||
|
debugPrint('[$tag] $message');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_entries.add(
|
_entries.add(
|
||||||
AppDebugLogEntry(
|
AppDebugLogEntry(
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ class AppSettingsService extends ChangeNotifier {
|
|||||||
return stored ?? 'nmc';
|
return stored ?? 'nmc';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String batteryChemistryForRepeater(String repeaterPubKeyHex) {
|
||||||
|
final stored = _settings.batteryChemistryByRepeaterId[repeaterPubKeyHex];
|
||||||
|
if (stored == 'liion') return 'nmc';
|
||||||
|
return stored ?? 'nmc';
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> loadSettings() async {
|
Future<void> loadSettings() async {
|
||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
final jsonStr = prefs.getString(_settingsKey);
|
final jsonStr = prefs.getString(_settingsKey);
|
||||||
@@ -74,6 +80,14 @@ class AppSettingsService extends ChangeNotifier {
|
|||||||
await updateSettings(_settings.copyWith(mapShowMarkers: value));
|
await updateSettings(_settings.copyWith(mapShowMarkers: value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setMapShowGuessedLocations(bool value) async {
|
||||||
|
await updateSettings(_settings.copyWith(mapShowGuessedLocations: value));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setEnableMessageTracing(bool value) async {
|
||||||
|
await updateSettings(_settings.copyWith(enableMessageTracing: value));
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> setMapCacheBounds(Map<String, double>? value) async {
|
Future<void> setMapCacheBounds(Map<String, double>? value) async {
|
||||||
await updateSettings(_settings.copyWith(mapCacheBounds: value));
|
await updateSettings(_settings.copyWith(mapCacheBounds: value));
|
||||||
}
|
}
|
||||||
@@ -132,4 +146,36 @@ class AppSettingsService extends ChangeNotifier {
|
|||||||
_settings.copyWith(batteryChemistryByDeviceId: updated),
|
_settings.copyWith(batteryChemistryByDeviceId: updated),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setBatteryChemistryForRepeater(
|
||||||
|
String repeaterPubKeyHex,
|
||||||
|
String chemistry,
|
||||||
|
) async {
|
||||||
|
final updated = Map<String, String>.from(
|
||||||
|
_settings.batteryChemistryByRepeaterId,
|
||||||
|
);
|
||||||
|
updated[repeaterPubKeyHex] = chemistry;
|
||||||
|
await updateSettings(
|
||||||
|
_settings.copyWith(batteryChemistryByRepeaterId: updated),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setUnitSystem(UnitSystem value) async {
|
||||||
|
await updateSettings(_settings.copyWith(unitSystem: value));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isChannelMuted(String channelName) {
|
||||||
|
return _settings.mutedChannels.contains(channelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> muteChannel(String channelName) async {
|
||||||
|
final updated = Set<String>.from(_settings.mutedChannels)..add(channelName);
|
||||||
|
await updateSettings(_settings.copyWith(mutedChannels: updated));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> unmuteChannel(String channelName) async {
|
||||||
|
final updated = Set<String>.from(_settings.mutedChannels)
|
||||||
|
..remove(channelName);
|
||||||
|
await updateSettings(_settings.copyWith(mutedChannels: updated));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import 'dart:io';
|
import '../utils/platform_info.dart';
|
||||||
|
|
||||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||||
|
|
||||||
class BackgroundService {
|
class BackgroundService {
|
||||||
bool _initialized = false;
|
bool _initialized = false;
|
||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
if (!Platform.isAndroid || _initialized) return;
|
if (!PlatformInfo.isAndroid || _initialized) return;
|
||||||
FlutterForegroundTask.init(
|
FlutterForegroundTask.init(
|
||||||
androidNotificationOptions: AndroidNotificationOptions(
|
androidNotificationOptions: AndroidNotificationOptions(
|
||||||
channelId: 'meshcore_background',
|
channelId: 'meshcore_background',
|
||||||
@@ -29,7 +28,7 @@ class BackgroundService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> start() async {
|
Future<void> start() async {
|
||||||
if (!Platform.isAndroid) return;
|
if (!PlatformInfo.isAndroid) return;
|
||||||
if (!_initialized) {
|
if (!_initialized) {
|
||||||
await initialize();
|
await initialize();
|
||||||
}
|
}
|
||||||
@@ -43,7 +42,7 @@ class BackgroundService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> stop() async {
|
Future<void> stop() async {
|
||||||
if (!Platform.isAndroid) return;
|
if (!PlatformInfo.isAndroid) return;
|
||||||
final running = await FlutterForegroundTask.isRunningService;
|
final running = await FlutterForegroundTask.isRunningService;
|
||||||
if (!running) return;
|
if (!running) return;
|
||||||
await FlutterForegroundTask.stopService();
|
await FlutterForegroundTask.stopService();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
|
|
||||||
class BleDebugLogEntry {
|
class BleDebugLogEntry {
|
||||||
@@ -44,6 +45,7 @@ class BleDebugLogService extends ChangeNotifier {
|
|||||||
static const int maxEntries = 500;
|
static const int maxEntries = 500;
|
||||||
final List<BleDebugLogEntry> _entries = [];
|
final List<BleDebugLogEntry> _entries = [];
|
||||||
final List<BleRawLogRxEntry> _rawLogRxEntries = [];
|
final List<BleRawLogRxEntry> _rawLogRxEntries = [];
|
||||||
|
bool _notifyScheduled = false;
|
||||||
|
|
||||||
List<BleDebugLogEntry> get entries => List.unmodifiable(_entries);
|
List<BleDebugLogEntry> get entries => List.unmodifiable(_entries);
|
||||||
List<BleRawLogRxEntry> get rawLogRxEntries =>
|
List<BleRawLogRxEntry> get rawLogRxEntries =>
|
||||||
@@ -78,13 +80,31 @@ class BleDebugLogService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyListeners();
|
_notifyListenersSafely();
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
_entries.clear();
|
_entries.clear();
|
||||||
_rawLogRxEntries.clear();
|
_rawLogRxEntries.clear();
|
||||||
notifyListeners();
|
_notifyListenersSafely();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _notifyListenersSafely() {
|
||||||
|
final phase = SchedulerBinding.instance.schedulerPhase;
|
||||||
|
final canNotifyNow =
|
||||||
|
phase == SchedulerPhase.idle ||
|
||||||
|
phase == SchedulerPhase.postFrameCallbacks;
|
||||||
|
if (canNotifyNow) {
|
||||||
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_notifyScheduled) return;
|
||||||
|
_notifyScheduled = true;
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_notifyScheduled = false;
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
String _describeFrame(
|
String _describeFrame(
|
||||||
@@ -152,8 +172,6 @@ class BleDebugLogService extends ChangeNotifier {
|
|||||||
return 'CMD_GET_CHANNEL';
|
return 'CMD_GET_CHANNEL';
|
||||||
case cmdSetChannel:
|
case cmdSetChannel:
|
||||||
return 'CMD_SET_CHANNEL';
|
return 'CMD_SET_CHANNEL';
|
||||||
case cmdGetRadioSettings:
|
|
||||||
return 'CMD_GET_RADIO_SETTINGS';
|
|
||||||
case cmdSetCustomVar:
|
case cmdSetCustomVar:
|
||||||
return 'CMD_SET_CUSTOM_VAR';
|
return 'CMD_SET_CUSTOM_VAR';
|
||||||
case cmdSendTracePath:
|
case cmdSendTracePath:
|
||||||
@@ -195,8 +213,8 @@ class BleDebugLogService extends ChangeNotifier {
|
|||||||
return 'RESP_CODE_CHANNEL_MSG_RECV_V3';
|
return 'RESP_CODE_CHANNEL_MSG_RECV_V3';
|
||||||
case respCodeChannelInfo:
|
case respCodeChannelInfo:
|
||||||
return 'RESP_CODE_CHANNEL_INFO';
|
return 'RESP_CODE_CHANNEL_INFO';
|
||||||
case respCodeRadioSettings:
|
case respCodeAutoAddConfig:
|
||||||
return 'RESP_CODE_RADIO_SETTINGS';
|
return 'RESP_CODE_AUTO_ADD_CONFIG';
|
||||||
case pushCodeTraceData:
|
case pushCodeTraceData:
|
||||||
return 'PUSH_CODE_TRACE_DATA';
|
return 'PUSH_CODE_TRACE_DATA';
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import '../storage/prefs_manager.dart';
|
||||||
|
|
||||||
|
/// Client-side accessibility/UI service that exposes a persistent shared text scale
|
||||||
|
/// factor. No MeshCoreConnector/RoomServer or protocol interaction occurs, and the
|
||||||
|
/// value is saved locally via SharedPreferences so it can be reused in Markdown
|
||||||
|
/// viewers, log panels, or other text-heavy widgets without redundant network
|
||||||
|
/// dependencies.
|
||||||
|
///
|
||||||
|
/// Widgets should scope rebuilds using the snippet below so only the scaled text
|
||||||
|
/// is rebuilt instead of the entire chat list:
|
||||||
|
/// ```dart
|
||||||
|
/// context.select<ChatTextScaleService, double>(
|
||||||
|
/// (service) => service.scale,
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
class ChatTextScaleService extends ChangeNotifier {
|
||||||
|
static const _prefKey = 'chat_text_scale';
|
||||||
|
static const double _minScale = 0.8;
|
||||||
|
static const double _maxScale = 1.8;
|
||||||
|
|
||||||
|
double _scale = 1.0;
|
||||||
|
Timer? _saveTimer;
|
||||||
|
|
||||||
|
double get scale => _scale;
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
final stored = PrefsManager.instance.getDouble(_prefKey);
|
||||||
|
if (stored != null) {
|
||||||
|
_scale = _clamp(stored);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setScale(double value, {bool persistImmediately = false}) {
|
||||||
|
final next = _clamp(value);
|
||||||
|
if (next == _scale) return;
|
||||||
|
_scale = next;
|
||||||
|
notifyListeners();
|
||||||
|
if (persistImmediately) {
|
||||||
|
_commitScale();
|
||||||
|
} else {
|
||||||
|
_scheduleSave();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
setScale(1.0, persistImmediately: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void persist() => _commitScale();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_saveTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scheduleSave() {
|
||||||
|
_saveTimer?.cancel();
|
||||||
|
_saveTimer = Timer(const Duration(milliseconds: 250), _commitScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _commitScale() {
|
||||||
|
_saveTimer?.cancel();
|
||||||
|
PrefsManager.instance.setDouble(_prefKey, _scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _clamp(double value) => value.clamp(_minScale, _maxScale).toDouble();
|
||||||
|
}
|
||||||
@@ -0,0 +1,446 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
|
typedef ElevationDataSource =
|
||||||
|
Future<List<double?>> Function(List<LatLng> points);
|
||||||
|
|
||||||
|
class LineOfSightSample {
|
||||||
|
final double distanceMeters;
|
||||||
|
final double terrainMeters;
|
||||||
|
final double lineHeightMeters;
|
||||||
|
final double refractedHeightMeters;
|
||||||
|
final double clearanceMeters;
|
||||||
|
|
||||||
|
const LineOfSightSample({
|
||||||
|
required this.distanceMeters,
|
||||||
|
required this.terrainMeters,
|
||||||
|
required this.lineHeightMeters,
|
||||||
|
required this.refractedHeightMeters,
|
||||||
|
required this.clearanceMeters,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class LineOfSightResult {
|
||||||
|
final bool hasData;
|
||||||
|
final bool isClear;
|
||||||
|
final double totalDistanceMeters;
|
||||||
|
final double maxObstructionMeters;
|
||||||
|
final double? firstObstructionDistanceMeters;
|
||||||
|
final List<LineOfSightSample> samples;
|
||||||
|
final String? errorMessage;
|
||||||
|
final double usedKFactor;
|
||||||
|
final double? frequencyMHz;
|
||||||
|
|
||||||
|
const LineOfSightResult({
|
||||||
|
required this.hasData,
|
||||||
|
required this.isClear,
|
||||||
|
required this.totalDistanceMeters,
|
||||||
|
required this.maxObstructionMeters,
|
||||||
|
required this.firstObstructionDistanceMeters,
|
||||||
|
required this.samples,
|
||||||
|
required this.usedKFactor,
|
||||||
|
this.frequencyMHz,
|
||||||
|
this.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const LineOfSightResult.error({
|
||||||
|
required this.totalDistanceMeters,
|
||||||
|
required this.errorMessage,
|
||||||
|
this.usedKFactor = 4.0 / 3.0,
|
||||||
|
this.frequencyMHz,
|
||||||
|
}) : hasData = false,
|
||||||
|
isClear = false,
|
||||||
|
maxObstructionMeters = 0,
|
||||||
|
firstObstructionDistanceMeters = null,
|
||||||
|
samples = const [];
|
||||||
|
}
|
||||||
|
|
||||||
|
class LineOfSightPathSegment {
|
||||||
|
final int index;
|
||||||
|
final LatLng start;
|
||||||
|
final LatLng end;
|
||||||
|
final LineOfSightResult result;
|
||||||
|
|
||||||
|
const LineOfSightPathSegment({
|
||||||
|
required this.index,
|
||||||
|
required this.start,
|
||||||
|
required this.end,
|
||||||
|
required this.result,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class LineOfSightPathResult {
|
||||||
|
final List<LineOfSightPathSegment> segments;
|
||||||
|
final int clearSegments;
|
||||||
|
final int blockedSegments;
|
||||||
|
final int unknownSegments;
|
||||||
|
|
||||||
|
const LineOfSightPathResult({
|
||||||
|
required this.segments,
|
||||||
|
required this.clearSegments,
|
||||||
|
required this.blockedSegments,
|
||||||
|
required this.unknownSegments,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class LineOfSightService {
|
||||||
|
static const String errorElevationUnavailable =
|
||||||
|
'los_error_elevation_unavailable';
|
||||||
|
static const String errorInvalidInput = 'los_error_invalid_input';
|
||||||
|
|
||||||
|
static const double _earthRadiusMeters = 6371000.0;
|
||||||
|
static const Distance _distance = Distance();
|
||||||
|
static const Duration _cacheTtl = Duration(hours: 24);
|
||||||
|
static const int _maxFetchAttempts = 4; // initial try + 3 retries
|
||||||
|
static const Duration _initialBackoff = Duration(milliseconds: 300);
|
||||||
|
static const double _baselineFrequencyMHz = 915.0;
|
||||||
|
static const double _baselineKFactor = 4.0 / 3.0;
|
||||||
|
|
||||||
|
static double get baselineFrequencyMHz => _baselineFrequencyMHz;
|
||||||
|
static double get baselineKFactor => _baselineKFactor;
|
||||||
|
|
||||||
|
final http.Client _httpClient;
|
||||||
|
final bool _ownsHttpClient;
|
||||||
|
final ElevationDataSource? _elevationDataSource;
|
||||||
|
final Map<String, _CachedElevation> _elevationCache = {};
|
||||||
|
|
||||||
|
LineOfSightService({
|
||||||
|
http.Client? httpClient,
|
||||||
|
ElevationDataSource? elevationDataSource,
|
||||||
|
}) : _httpClient = httpClient ?? http.Client(),
|
||||||
|
_ownsHttpClient = httpClient == null,
|
||||||
|
_elevationDataSource = elevationDataSource;
|
||||||
|
|
||||||
|
Future<LineOfSightPathResult> analyzePath(
|
||||||
|
List<LatLng> points, {
|
||||||
|
double startAntennaHeightMeters = 1.5,
|
||||||
|
double endAntennaHeightMeters = 1.5,
|
||||||
|
double? frequencyMHz,
|
||||||
|
double obstructionToleranceMeters = 0.0,
|
||||||
|
}) async {
|
||||||
|
if (points.length < 2) {
|
||||||
|
return const LineOfSightPathResult(
|
||||||
|
segments: [],
|
||||||
|
clearSegments: 0,
|
||||||
|
blockedSegments: 0,
|
||||||
|
unknownSegments: 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final segments = <LineOfSightPathSegment>[];
|
||||||
|
var clearSegments = 0;
|
||||||
|
var blockedSegments = 0;
|
||||||
|
var unknownSegments = 0;
|
||||||
|
|
||||||
|
final kFactor = _kFactorForFrequency(frequencyMHz);
|
||||||
|
for (int i = 0; i < points.length - 1; i++) {
|
||||||
|
final result = await analyzeLink(
|
||||||
|
points[i],
|
||||||
|
points[i + 1],
|
||||||
|
startAntennaHeightMeters: startAntennaHeightMeters,
|
||||||
|
endAntennaHeightMeters: endAntennaHeightMeters,
|
||||||
|
kFactor: kFactor,
|
||||||
|
frequencyMHz: frequencyMHz,
|
||||||
|
obstructionToleranceMeters: obstructionToleranceMeters,
|
||||||
|
);
|
||||||
|
segments.add(
|
||||||
|
LineOfSightPathSegment(
|
||||||
|
index: i,
|
||||||
|
start: points[i],
|
||||||
|
end: points[i + 1],
|
||||||
|
result: result,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.hasData) {
|
||||||
|
unknownSegments++;
|
||||||
|
} else if (result.isClear) {
|
||||||
|
clearSegments++;
|
||||||
|
} else {
|
||||||
|
blockedSegments++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return LineOfSightPathResult(
|
||||||
|
segments: segments,
|
||||||
|
clearSegments: clearSegments,
|
||||||
|
blockedSegments: blockedSegments,
|
||||||
|
unknownSegments: unknownSegments,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<LineOfSightResult> analyzeLink(
|
||||||
|
LatLng start,
|
||||||
|
LatLng end, {
|
||||||
|
double startAntennaHeightMeters = 1.5,
|
||||||
|
double endAntennaHeightMeters = 1.5,
|
||||||
|
required double kFactor,
|
||||||
|
double? frequencyMHz,
|
||||||
|
double obstructionToleranceMeters = 0.0,
|
||||||
|
}) async {
|
||||||
|
final totalDistanceMeters = _distance.as(LengthUnit.Meter, start, end);
|
||||||
|
if (totalDistanceMeters <= 1) {
|
||||||
|
return LineOfSightResult(
|
||||||
|
hasData: true,
|
||||||
|
isClear: true,
|
||||||
|
totalDistanceMeters: totalDistanceMeters,
|
||||||
|
maxObstructionMeters: 0,
|
||||||
|
firstObstructionDistanceMeters: null,
|
||||||
|
samples: const [],
|
||||||
|
usedKFactor: kFactor,
|
||||||
|
frequencyMHz: frequencyMHz,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final samplePoints = _buildSamplePoints(start, end, totalDistanceMeters);
|
||||||
|
final elevations = await _getElevations(samplePoints);
|
||||||
|
|
||||||
|
if (elevations.any((e) => e == null)) {
|
||||||
|
return LineOfSightResult.error(
|
||||||
|
totalDistanceMeters: totalDistanceMeters,
|
||||||
|
errorMessage: errorElevationUnavailable,
|
||||||
|
usedKFactor: kFactor,
|
||||||
|
frequencyMHz: frequencyMHz,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return computeFromElevations(
|
||||||
|
points: samplePoints,
|
||||||
|
elevations: elevations.cast<double>(),
|
||||||
|
startAntennaHeightMeters: startAntennaHeightMeters,
|
||||||
|
endAntennaHeightMeters: endAntennaHeightMeters,
|
||||||
|
kFactor: kFactor,
|
||||||
|
frequencyMHz: frequencyMHz,
|
||||||
|
obstructionToleranceMeters: obstructionToleranceMeters,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static LineOfSightResult computeFromElevations({
|
||||||
|
required List<LatLng> points,
|
||||||
|
required List<double> elevations,
|
||||||
|
double startAntennaHeightMeters = 1.5,
|
||||||
|
double endAntennaHeightMeters = 1.5,
|
||||||
|
required double kFactor,
|
||||||
|
double? frequencyMHz,
|
||||||
|
double obstructionToleranceMeters = 0.0,
|
||||||
|
}) {
|
||||||
|
if (points.length < 2 || elevations.length != points.length) {
|
||||||
|
return LineOfSightResult.error(
|
||||||
|
totalDistanceMeters: 0,
|
||||||
|
errorMessage: errorInvalidInput,
|
||||||
|
usedKFactor: kFactor,
|
||||||
|
frequencyMHz: frequencyMHz,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final totalDistanceMeters = _distance.as(
|
||||||
|
LengthUnit.Meter,
|
||||||
|
points.first,
|
||||||
|
points.last,
|
||||||
|
);
|
||||||
|
final effectiveEarthRadius = _earthRadiusMeters * kFactor;
|
||||||
|
final startLineHeight = elevations.first + startAntennaHeightMeters;
|
||||||
|
final endLineHeight = elevations.last + endAntennaHeightMeters;
|
||||||
|
|
||||||
|
var maxObstructionMeters = 0.0;
|
||||||
|
double? firstObstructionDistanceMeters;
|
||||||
|
final samples = <LineOfSightSample>[];
|
||||||
|
var isClear = true;
|
||||||
|
|
||||||
|
for (int i = 0; i < points.length; i++) {
|
||||||
|
final fraction = points.length == 1 ? 0.0 : i / (points.length - 1);
|
||||||
|
final distanceFromStart = totalDistanceMeters * fraction;
|
||||||
|
final lineHeight =
|
||||||
|
startLineHeight + (endLineHeight - startLineHeight) * fraction;
|
||||||
|
|
||||||
|
final earthBulge =
|
||||||
|
(distanceFromStart * (totalDistanceMeters - distanceFromStart)) /
|
||||||
|
(2 * effectiveEarthRadius);
|
||||||
|
final terrainHeight = elevations[i] + earthBulge;
|
||||||
|
final clearance = lineHeight - terrainHeight;
|
||||||
|
final unrefBulge =
|
||||||
|
(distanceFromStart * (totalDistanceMeters - distanceFromStart)) /
|
||||||
|
(2 * _earthRadiusMeters);
|
||||||
|
final refractedHeight = lineHeight + (unrefBulge - earthBulge);
|
||||||
|
|
||||||
|
if (clearance < -obstructionToleranceMeters) {
|
||||||
|
isClear = false;
|
||||||
|
final obstruction = -clearance;
|
||||||
|
if (obstruction > maxObstructionMeters) {
|
||||||
|
maxObstructionMeters = obstruction;
|
||||||
|
}
|
||||||
|
firstObstructionDistanceMeters ??= distanceFromStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
samples.add(
|
||||||
|
LineOfSightSample(
|
||||||
|
distanceMeters: distanceFromStart,
|
||||||
|
terrainMeters: terrainHeight,
|
||||||
|
lineHeightMeters: lineHeight,
|
||||||
|
refractedHeightMeters: refractedHeight,
|
||||||
|
clearanceMeters: clearance,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return LineOfSightResult(
|
||||||
|
hasData: true,
|
||||||
|
isClear: isClear,
|
||||||
|
totalDistanceMeters: totalDistanceMeters,
|
||||||
|
maxObstructionMeters: maxObstructionMeters,
|
||||||
|
firstObstructionDistanceMeters: firstObstructionDistanceMeters,
|
||||||
|
samples: samples,
|
||||||
|
usedKFactor: kFactor,
|
||||||
|
frequencyMHz: frequencyMHz,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static double _kFactorForFrequency(double? frequencyMHz) {
|
||||||
|
if (frequencyMHz == null) return _baselineKFactor;
|
||||||
|
final delta =
|
||||||
|
(frequencyMHz - _baselineFrequencyMHz) / _baselineFrequencyMHz;
|
||||||
|
final adjustment = delta * 0.15;
|
||||||
|
final scaled = _baselineKFactor * (1 + adjustment);
|
||||||
|
return scaled.clamp(1.1, 1.6).toDouble();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LatLng> _buildSamplePoints(
|
||||||
|
LatLng start,
|
||||||
|
LatLng end,
|
||||||
|
double distanceMeters,
|
||||||
|
) {
|
||||||
|
final sampleCount = distanceMeters < 2000
|
||||||
|
? 21
|
||||||
|
: distanceMeters < 10000
|
||||||
|
? 41
|
||||||
|
: 81;
|
||||||
|
|
||||||
|
final points = <LatLng>[];
|
||||||
|
for (int i = 0; i < sampleCount; i++) {
|
||||||
|
final t = i / (sampleCount - 1);
|
||||||
|
points.add(
|
||||||
|
LatLng(
|
||||||
|
start.latitude + (end.latitude - start.latitude) * t,
|
||||||
|
start.longitude + (end.longitude - start.longitude) * t,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<double?>> _getElevations(List<LatLng> points) async {
|
||||||
|
final dataSource = _elevationDataSource;
|
||||||
|
if (dataSource != null) {
|
||||||
|
return dataSource(points);
|
||||||
|
}
|
||||||
|
|
||||||
|
final uncached = <int, LatLng>{};
|
||||||
|
final values = List<double?>.filled(points.length, null);
|
||||||
|
for (int i = 0; i < points.length; i++) {
|
||||||
|
final key = _cacheKey(points[i]);
|
||||||
|
final cached = _readCachedValue(key);
|
||||||
|
if (cached != null) {
|
||||||
|
values[i] = cached;
|
||||||
|
} else {
|
||||||
|
uncached[i] = points[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uncached.isEmpty) return values;
|
||||||
|
|
||||||
|
final latCsv = uncached.values
|
||||||
|
.map((p) => p.latitude.toStringAsFixed(6))
|
||||||
|
.join(',');
|
||||||
|
final lonCsv = uncached.values
|
||||||
|
.map((p) => p.longitude.toStringAsFixed(6))
|
||||||
|
.join(',');
|
||||||
|
|
||||||
|
final uri = Uri.parse(
|
||||||
|
'https://api.open-meteo.com/v1/elevation?latitude=$latCsv&longitude=$lonCsv',
|
||||||
|
);
|
||||||
|
|
||||||
|
final response = await _getWithBackoff(uri);
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
final decoded = jsonDecode(response.body);
|
||||||
|
if (decoded is! Map<String, dynamic>) {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
final elevations = decoded['elevation'];
|
||||||
|
if (elevations is! List) {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
final indices = uncached.keys.toList();
|
||||||
|
for (int i = 0; i < min(indices.length, elevations.length); i++) {
|
||||||
|
final value = elevations[i];
|
||||||
|
if (value is! num) continue;
|
||||||
|
final index = indices[i];
|
||||||
|
final elevation = value.toDouble();
|
||||||
|
values[index] = elevation;
|
||||||
|
_elevationCache[_cacheKey(points[index])] = _CachedElevation(
|
||||||
|
value: elevation,
|
||||||
|
expiresAt: DateTime.now().add(_cacheTtl),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<http.Response> _getWithBackoff(Uri uri) async {
|
||||||
|
var attempt = 0;
|
||||||
|
Duration backoff = _initialBackoff;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
attempt++;
|
||||||
|
try {
|
||||||
|
final response = await _httpClient.get(uri);
|
||||||
|
if (!_shouldRetryStatus(response.statusCode) ||
|
||||||
|
attempt >= _maxFetchAttempts) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
if (attempt >= _maxFetchAttempts) rethrow;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Future.delayed(backoff);
|
||||||
|
backoff *= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _shouldRetryStatus(int statusCode) {
|
||||||
|
return statusCode == 429 || statusCode >= 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
double? _readCachedValue(String key) {
|
||||||
|
final cached = _elevationCache[key];
|
||||||
|
if (cached == null) return null;
|
||||||
|
if (DateTime.now().isAfter(cached.expiresAt)) {
|
||||||
|
_elevationCache.remove(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return cached.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _cacheKey(LatLng point) {
|
||||||
|
return '${point.latitude.toStringAsFixed(5)},${point.longitude.toStringAsFixed(5)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
if (_ownsHttpClient) {
|
||||||
|
_httpClient.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CachedElevation {
|
||||||
|
final double value;
|
||||||
|
final DateTime expiresAt;
|
||||||
|
|
||||||
|
const _CachedElevation({required this.value, required this.expiresAt});
|
||||||
|
}
|
||||||
@@ -234,7 +234,11 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateMessageFromSent(Uint8List ackHash, int timeoutMs) {
|
bool updateMessageFromSent(
|
||||||
|
Uint8List ackHash,
|
||||||
|
int timeoutMs, {
|
||||||
|
bool allowQueueFallback = true,
|
||||||
|
}) {
|
||||||
final ackHashHex = ackHash
|
final ackHashHex = ackHash
|
||||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
.join();
|
.join();
|
||||||
@@ -277,7 +281,7 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FALLBACK: Old queue-based matching (for messages sent before hash computation was added)
|
// FALLBACK: Old queue-based matching (for messages sent before hash computation was added)
|
||||||
if (messageId == null) {
|
if (messageId == null && allowQueueFallback) {
|
||||||
_debugLogService?.warn(
|
_debugLogService?.warn(
|
||||||
'RESP_CODE_SENT: ACK hash $ackHashHex not found in hash table, falling back to queue',
|
'RESP_CODE_SENT: ACK hash $ackHashHex not found in hash table, falling back to queue',
|
||||||
tag: 'AckHash',
|
tag: 'AckHash',
|
||||||
@@ -320,7 +324,7 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
|
|
||||||
if (messageId == null || contact == null) {
|
if (messageId == null || contact == null) {
|
||||||
debugPrint('No pending message found for ACK hash: $ackHashHex');
|
debugPrint('No pending message found for ACK hash: $ackHashHex');
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the mapping for future lookups (e.g., when ACK arrives)
|
// Store the mapping for future lookups (e.g., when ACK arrives)
|
||||||
@@ -339,7 +343,7 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
'Message $messageId no longer pending for ACK hash: $ackHashHex',
|
'Message $messageId no longer pending for ACK hash: $ackHashHex',
|
||||||
);
|
);
|
||||||
_ackHashToMessageId.remove(ackHashHex);
|
_ackHashToMessageId.remove(ackHashHex);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add this ACK hash to the list of expected ACKs for this message (for history)
|
// Add this ACK hash to the list of expected ACKs for this message (for history)
|
||||||
@@ -389,8 +393,11 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
|
|
||||||
_startTimeoutTimer(messageId, actualTimeout);
|
_startTimeoutTimer(messageId, actualTimeout);
|
||||||
debugPrint('Updated message $messageId with ACK hash: $ackHashHex');
|
debugPrint('Updated message $messageId with ACK hash: $ackHashHex');
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get hasPendingMessages => _pendingMessages.isNotEmpty;
|
||||||
|
|
||||||
void _startTimeoutTimer(String messageId, int timeoutMs) {
|
void _startTimeoutTimer(String messageId, int timeoutMs) {
|
||||||
_timeoutTimers[messageId]?.cancel();
|
_timeoutTimers[messageId]?.cancel();
|
||||||
_timeoutTimers[messageId] = Timer(Duration(milliseconds: timeoutMs), () {
|
_timeoutTimers[messageId] = Timer(Duration(milliseconds: timeoutMs), () {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import 'dart:io' show Platform, File;
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
|
import '../utils/platform_info.dart';
|
||||||
|
|
||||||
class NotificationService {
|
class NotificationService {
|
||||||
static final NotificationService _instance = NotificationService._internal();
|
static final NotificationService _instance = NotificationService._internal();
|
||||||
@@ -58,13 +60,32 @@ class NotificationService {
|
|||||||
requestBadgePermission: true,
|
requestBadgePermission: true,
|
||||||
requestSoundPermission: true,
|
requestSoundPermission: true,
|
||||||
);
|
);
|
||||||
|
const windowsSettings = WindowsInitializationSettings(
|
||||||
|
appName: 'MeshCore Open',
|
||||||
|
appUserModelId: 'org.meshcore.open.app',
|
||||||
|
guid: 'e7ea8f85-72f5-4f36-91f6-038f740ccf86',
|
||||||
|
);
|
||||||
|
const linuxSettings = LinuxInitializationSettings(
|
||||||
|
defaultActionName: 'Open notification',
|
||||||
|
);
|
||||||
|
|
||||||
const initSettings = InitializationSettings(
|
const initSettings = InitializationSettings(
|
||||||
android: androidSettings,
|
android: androidSettings,
|
||||||
iOS: iosSettings,
|
iOS: iosSettings,
|
||||||
macOS: macSettings,
|
macOS: macSettings,
|
||||||
|
windows: windowsSettings,
|
||||||
|
linux: linuxSettings,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// On Linux, the notifications plugin opens a D-Bus session bus
|
||||||
|
// connection whose async subscription can throw an unhandled
|
||||||
|
// SocketException when the bus socket is missing (e.g. running as
|
||||||
|
// root or inside a container without a session bus).
|
||||||
|
if (PlatformInfo.isLinux && !_isDbusSessionAvailable()) {
|
||||||
|
debugPrint('Skipping notification init: D-Bus session bus unavailable');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _notifications.initialize(
|
await _notifications.initialize(
|
||||||
settings: initSettings,
|
settings: initSettings,
|
||||||
@@ -76,6 +97,23 @@ class NotificationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool _isDbusSessionAvailable() {
|
||||||
|
final addr = Platform.environment['DBUS_SESSION_BUS_ADDRESS'];
|
||||||
|
if (addr != null && addr.isNotEmpty) return true;
|
||||||
|
// Fallback: check the default socket for the current user.
|
||||||
|
final uid = Platform.environment['UID'] ??
|
||||||
|
Platform.environment['EUID'];
|
||||||
|
final path = '/run/user/${uid ?? '1000'}/bus';
|
||||||
|
return File(path).existsSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _ensureInitialized() async {
|
||||||
|
if (!_isInitialized) {
|
||||||
|
await initialize();
|
||||||
|
}
|
||||||
|
return _isInitialized;
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> requestPermissions() async {
|
Future<bool> requestPermissions() async {
|
||||||
if (!_isInitialized) {
|
if (!_isInitialized) {
|
||||||
await initialize();
|
await initialize();
|
||||||
@@ -114,9 +152,7 @@ class NotificationService {
|
|||||||
String? contactId,
|
String? contactId,
|
||||||
int? badgeCount,
|
int? badgeCount,
|
||||||
}) async {
|
}) async {
|
||||||
if (!_isInitialized) {
|
if (!await _ensureInitialized()) return;
|
||||||
await initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
final androidDetails = AndroidNotificationDetails(
|
final androidDetails = AndroidNotificationDetails(
|
||||||
'messages',
|
'messages',
|
||||||
@@ -148,13 +184,17 @@ class NotificationService {
|
|||||||
macOS: macDetails,
|
macOS: macDetails,
|
||||||
);
|
);
|
||||||
|
|
||||||
await _notifications.show(
|
try {
|
||||||
id: contactId?.hashCode ?? 0,
|
await _notifications.show(
|
||||||
title: contactName,
|
id: contactId?.hashCode ?? 0,
|
||||||
body: message,
|
title: contactName,
|
||||||
notificationDetails: notificationDetails,
|
body: message,
|
||||||
payload: 'message:$contactId',
|
notificationDetails: notificationDetails,
|
||||||
);
|
payload: 'message:$contactId',
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Failed to show message notification: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showAdvertNotificationImpl({
|
Future<void> _showAdvertNotificationImpl({
|
||||||
@@ -162,9 +202,7 @@ class NotificationService {
|
|||||||
required String contactType,
|
required String contactType,
|
||||||
String? contactId,
|
String? contactId,
|
||||||
}) async {
|
}) async {
|
||||||
if (!_isInitialized) {
|
if (!await _ensureInitialized()) return;
|
||||||
await initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
const androidDetails = AndroidNotificationDetails(
|
const androidDetails = AndroidNotificationDetails(
|
||||||
'adverts',
|
'adverts',
|
||||||
@@ -193,13 +231,17 @@ class NotificationService {
|
|||||||
macOS: macDetails,
|
macOS: macDetails,
|
||||||
);
|
);
|
||||||
|
|
||||||
await _notifications.show(
|
try {
|
||||||
id: contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
|
await _notifications.show(
|
||||||
title: _l10n.notification_newTypeDiscovered(contactType),
|
id: contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
|
||||||
body: contactName,
|
title: _l10n.notification_newTypeDiscovered(contactType),
|
||||||
notificationDetails: notificationDetails,
|
body: contactName,
|
||||||
payload: 'advert:$contactId',
|
notificationDetails: notificationDetails,
|
||||||
);
|
payload: 'advert:$contactId',
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Failed to show advert notification: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showChannelMessageNotificationImpl({
|
Future<void> _showChannelMessageNotificationImpl({
|
||||||
@@ -208,9 +250,7 @@ class NotificationService {
|
|||||||
int? channelIndex,
|
int? channelIndex,
|
||||||
int? badgeCount,
|
int? badgeCount,
|
||||||
}) async {
|
}) async {
|
||||||
if (!_isInitialized) {
|
if (!await _ensureInitialized()) return;
|
||||||
await initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
final androidDetails = AndroidNotificationDetails(
|
final androidDetails = AndroidNotificationDetails(
|
||||||
'channel_messages',
|
'channel_messages',
|
||||||
@@ -247,13 +287,17 @@ class NotificationService {
|
|||||||
? _l10n.notification_receivedNewMessage
|
? _l10n.notification_receivedNewMessage
|
||||||
: preview;
|
: preview;
|
||||||
|
|
||||||
await _notifications.show(
|
try {
|
||||||
id: channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
|
await _notifications.show(
|
||||||
title: channelName,
|
id: channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
|
||||||
body: body,
|
title: channelName,
|
||||||
notificationDetails: notificationDetails,
|
body: body,
|
||||||
payload: 'channel:$channelIndex',
|
notificationDetails: notificationDetails,
|
||||||
);
|
payload: 'channel:$channelIndex',
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Failed to show channel notification: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a privacy-safe identifier for debug logging.
|
/// Returns a privacy-safe identifier for debug logging.
|
||||||
@@ -396,35 +440,39 @@ class NotificationService {
|
|||||||
Future<void> _showNotificationImmediately(
|
Future<void> _showNotificationImmediately(
|
||||||
_PendingNotification notification,
|
_PendingNotification notification,
|
||||||
) async {
|
) async {
|
||||||
switch (notification.type) {
|
try {
|
||||||
case _NotificationType.message:
|
switch (notification.type) {
|
||||||
await _showMessageNotificationImpl(
|
case _NotificationType.message:
|
||||||
contactName: notification.title,
|
await _showMessageNotificationImpl(
|
||||||
message: notification.body,
|
contactName: notification.title,
|
||||||
contactId: notification.id,
|
message: notification.body,
|
||||||
badgeCount: notification.badgeCount,
|
contactId: notification.id,
|
||||||
);
|
badgeCount: notification.badgeCount,
|
||||||
break;
|
);
|
||||||
case _NotificationType.advert:
|
break;
|
||||||
await _showAdvertNotificationImpl(
|
case _NotificationType.advert:
|
||||||
contactName: notification.body,
|
await _showAdvertNotificationImpl(
|
||||||
contactType: notification.title,
|
contactName: notification.body,
|
||||||
contactId: notification.id,
|
contactType: notification.title,
|
||||||
);
|
contactId: notification.id,
|
||||||
break;
|
);
|
||||||
case _NotificationType.channelMessage:
|
break;
|
||||||
await _showChannelMessageNotificationImpl(
|
case _NotificationType.channelMessage:
|
||||||
channelName: notification.title,
|
await _showChannelMessageNotificationImpl(
|
||||||
message: notification.body,
|
channelName: notification.title,
|
||||||
channelIndex: int.tryParse(notification.id ?? ''),
|
message: notification.body,
|
||||||
badgeCount: notification.badgeCount,
|
channelIndex: int.tryParse(notification.id ?? ''),
|
||||||
);
|
badgeCount: notification.badgeCount,
|
||||||
break;
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Failed to show immediate notification: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showBatchSummary(List<_PendingNotification> batch) async {
|
Future<void> _showBatchSummary(List<_PendingNotification> batch) async {
|
||||||
if (!_isInitialized) await initialize();
|
if (!await _ensureInitialized()) return;
|
||||||
|
|
||||||
// Group by type
|
// Group by type
|
||||||
final messages = batch
|
final messages = batch
|
||||||
@@ -468,13 +516,17 @@ class NotificationService {
|
|||||||
|
|
||||||
const notificationDetails = NotificationDetails(android: androidDetails);
|
const notificationDetails = NotificationDetails(android: androidDetails);
|
||||||
|
|
||||||
await _notifications.show(
|
try {
|
||||||
id: 'batch_summary'.hashCode,
|
await _notifications.show(
|
||||||
title: _l10n.notification_activityTitle,
|
id: 'batch_summary'.hashCode,
|
||||||
body: parts.join(', '),
|
title: _l10n.notification_activityTitle,
|
||||||
notificationDetails: notificationDetails,
|
body: parts.join(', '),
|
||||||
payload: 'batch',
|
notificationDetails: notificationDetails,
|
||||||
);
|
payload: 'batch',
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Failed to show batch summary notification: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
final List<String> _cacheAccessOrder = [];
|
final List<String> _cacheAccessOrder = [];
|
||||||
|
|
||||||
static const int _maxHistoryEntries = 100;
|
static const int _maxHistoryEntries = 100;
|
||||||
|
|
||||||
|
int _version = 0;
|
||||||
|
int get version => _version;
|
||||||
static const int _autoRotationTopCount = 3;
|
static const int _autoRotationTopCount = 3;
|
||||||
|
|
||||||
PathHistoryService(this._storage);
|
PathHistoryService(this._storage);
|
||||||
@@ -185,6 +188,7 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
) {
|
) {
|
||||||
var history = _cache[contactPubKeyHex];
|
var history = _cache[contactPubKeyHex];
|
||||||
if (history == null) return;
|
if (history == null) return;
|
||||||
|
_version++;
|
||||||
|
|
||||||
final existing = _findPathRecord(contactPubKeyHex, pathBytes);
|
final existing = _findPathRecord(contactPubKeyHex, pathBytes);
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
@@ -241,6 +245,7 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
_cache[contactPubKeyHex] = loaded;
|
_cache[contactPubKeyHex] = loaded;
|
||||||
_trackAccess(contactPubKeyHex);
|
_trackAccess(contactPubKeyHex);
|
||||||
_evictIfNeeded();
|
_evictIfNeeded();
|
||||||
|
_version++;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -276,6 +281,7 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
_autoRotationIndex.remove(contactPubKeyHex);
|
_autoRotationIndex.remove(contactPubKeyHex);
|
||||||
_floodStats.remove(contactPubKeyHex);
|
_floodStats.remove(contactPubKeyHex);
|
||||||
await _storage.clearPathHistory(contactPubKeyHex);
|
await _storage.clearPathHistory(contactPubKeyHex);
|
||||||
|
_version++;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,6 +301,7 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await _storage.savePathHistory(contactPubKeyHex, _cache[contactPubKeyHex]!);
|
await _storage.savePathHistory(contactPubKeyHex, _cache[contactPubKeyHex]!);
|
||||||
|
_version++;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
const int usbSerialTxFrameStart = 0x3c;
|
||||||
|
const int usbSerialRxFrameStart = 0x3e;
|
||||||
|
const int usbSerialHeaderLength = 3;
|
||||||
|
const int usbSerialMaxPayloadLength = 172;
|
||||||
|
|
||||||
|
Uint8List wrapUsbSerialTxFrame(Uint8List payload) {
|
||||||
|
if (payload.length > usbSerialMaxPayloadLength) {
|
||||||
|
throw ArgumentError.value(
|
||||||
|
payload.length,
|
||||||
|
'payload.length',
|
||||||
|
'USB serial payload exceeds $usbSerialMaxPayloadLength bytes',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final packet = Uint8List(usbSerialHeaderLength + payload.length);
|
||||||
|
packet[0] = usbSerialTxFrameStart;
|
||||||
|
packet[1] = payload.length & 0xff;
|
||||||
|
packet[2] = (payload.length >> 8) & 0xff;
|
||||||
|
packet.setRange(usbSerialHeaderLength, packet.length, payload);
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
class UsbSerialDecodedPacket {
|
||||||
|
const UsbSerialDecodedPacket({
|
||||||
|
required this.frameStart,
|
||||||
|
required this.payload,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int frameStart;
|
||||||
|
final Uint8List payload;
|
||||||
|
|
||||||
|
bool get isRxFrame => frameStart == usbSerialRxFrameStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
class UsbSerialFrameDecoder {
|
||||||
|
final List<int> _rxBuffer = <int>[];
|
||||||
|
int _startIndex = 0;
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
_rxBuffer.clear();
|
||||||
|
_startIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<UsbSerialDecodedPacket> ingest(Uint8List bytes) {
|
||||||
|
if (bytes.isEmpty) {
|
||||||
|
return const <UsbSerialDecodedPacket>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
_rxBuffer.addAll(bytes);
|
||||||
|
final packets = <UsbSerialDecodedPacket>[];
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (_startIndex >= _rxBuffer.length) {
|
||||||
|
_rxBuffer.clear();
|
||||||
|
_startIndex = 0;
|
||||||
|
return packets;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_rxBuffer[_startIndex] != usbSerialRxFrameStart &&
|
||||||
|
_rxBuffer[_startIndex] != usbSerialTxFrameStart) {
|
||||||
|
_startIndex++;
|
||||||
|
_compactBufferIfNeeded();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final availableLength = _rxBuffer.length - _startIndex;
|
||||||
|
if (availableLength < usbSerialHeaderLength) {
|
||||||
|
_compactBufferIfNeeded(force: true);
|
||||||
|
return packets;
|
||||||
|
}
|
||||||
|
|
||||||
|
final payloadLength =
|
||||||
|
_rxBuffer[_startIndex + 1] | (_rxBuffer[_startIndex + 2] << 8);
|
||||||
|
if (payloadLength > usbSerialMaxPayloadLength) {
|
||||||
|
_startIndex++;
|
||||||
|
_compactBufferIfNeeded();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final packetLength = usbSerialHeaderLength + payloadLength;
|
||||||
|
if (availableLength < packetLength) {
|
||||||
|
_compactBufferIfNeeded(force: true);
|
||||||
|
return packets;
|
||||||
|
}
|
||||||
|
|
||||||
|
final frameStart = _rxBuffer[_startIndex];
|
||||||
|
final payload = Uint8List.fromList(
|
||||||
|
_rxBuffer.sublist(
|
||||||
|
_startIndex + usbSerialHeaderLength,
|
||||||
|
_startIndex + packetLength,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_startIndex += packetLength;
|
||||||
|
_compactBufferIfNeeded();
|
||||||
|
packets.add(
|
||||||
|
UsbSerialDecodedPacket(frameStart: frameStart, payload: payload),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _compactBufferIfNeeded({bool force = false}) {
|
||||||
|
if (_startIndex == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!force && _startIndex < 1024 && _startIndex < (_rxBuffer.length ~/ 2)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_rxBuffer.removeRange(0, _startIndex);
|
||||||
|
_startIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export 'usb_serial_service_native.dart'
|
||||||
|
if (dart.library.js_interop) 'usb_serial_service_web.dart';
|
||||||
@@ -0,0 +1,463 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flserial/flserial.dart';
|
||||||
|
import 'package:flserial/flserial_exception.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import 'app_debug_log_service.dart';
|
||||||
|
import '../utils/macos_usb_device_names.dart';
|
||||||
|
import '../utils/platform_info.dart';
|
||||||
|
import '../utils/usb_port_labels.dart';
|
||||||
|
import 'usb_serial_frame_codec.dart';
|
||||||
|
|
||||||
|
/// Wraps the native flserial plugin to expose a stream of raw bytes for the
|
||||||
|
/// MeshCore connector to consume.
|
||||||
|
class UsbSerialService {
|
||||||
|
UsbSerialService();
|
||||||
|
|
||||||
|
static const MethodChannel _androidMethodChannel = MethodChannel(
|
||||||
|
'meshcore_open/android_usb_serial',
|
||||||
|
);
|
||||||
|
static const EventChannel _androidEventChannel = EventChannel(
|
||||||
|
'meshcore_open/android_usb_serial_events',
|
||||||
|
);
|
||||||
|
final StreamController<Uint8List> _frameController =
|
||||||
|
StreamController<Uint8List>.broadcast();
|
||||||
|
final UsbSerialFrameDecoder _frameDecoder = UsbSerialFrameDecoder();
|
||||||
|
StreamSubscription<dynamic>? _androidDataSubscription;
|
||||||
|
StreamSubscription<FlSerialEventArgs>? _dataSubscription;
|
||||||
|
UsbSerialStatus _status = UsbSerialStatus.disconnected;
|
||||||
|
String? _connectedPortKey;
|
||||||
|
String? _connectedPortLabel;
|
||||||
|
FlSerial? _serial;
|
||||||
|
AppDebugLogService? _debugLogService;
|
||||||
|
|
||||||
|
UsbSerialStatus get status => _status;
|
||||||
|
String? get activePortKey => _connectedPortKey;
|
||||||
|
String? get activePortDisplayLabel =>
|
||||||
|
_connectedPortLabel ?? _connectedPortKey;
|
||||||
|
Stream<Uint8List> get frameStream => _frameController.stream;
|
||||||
|
bool get _useAndroidUsbHost =>
|
||||||
|
!kIsWeb && defaultTargetPlatform == TargetPlatform.android;
|
||||||
|
bool get _useDesktopFlSerial =>
|
||||||
|
PlatformInfo.isWindows || PlatformInfo.isLinux || PlatformInfo.isMacOS;
|
||||||
|
bool get _isSupportedPlatform => _useAndroidUsbHost || _useDesktopFlSerial;
|
||||||
|
// Always-fresh: do NOT use ??= here – a cached FlSerial retains stale
|
||||||
|
// native handle state (flh) from a prior failed open, causing subsequent
|
||||||
|
// open attempts to fail with "port not exist" even when the device is present.
|
||||||
|
FlSerial _freshSerial() => FlSerial();
|
||||||
|
|
||||||
|
bool get isConnected {
|
||||||
|
if (!_isSupportedPlatform) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Trust _status as the authoritative connection state. Polling
|
||||||
|
// _serial?.isOpen() via the native FL_CTRL_IS_PORT_OPEN query is
|
||||||
|
// unreliable during the brief USB re-enumeration window that many
|
||||||
|
// microcontrollers (e.g. NRF52) trigger in response to DTR assertion.
|
||||||
|
// Actual port drops are handled by the onDone / onError callbacks on the
|
||||||
|
// serial data stream subscription, which update _status correctly.
|
||||||
|
return _status == UsbSerialStatus.connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> listPorts() async {
|
||||||
|
if (!_isSupportedPlatform) {
|
||||||
|
return const <String>[];
|
||||||
|
}
|
||||||
|
if (_useAndroidUsbHost) {
|
||||||
|
final ports = await _androidMethodChannel.invokeListMethod<String>(
|
||||||
|
'listPorts',
|
||||||
|
);
|
||||||
|
return ports ?? <String>[];
|
||||||
|
}
|
||||||
|
final rawPorts = FlSerial.listPorts();
|
||||||
|
// On macOS, flserial's native device-name lookup is broken on macOS
|
||||||
|
// 10.15+ because the IOKit class name changed from IOUSBDevice to
|
||||||
|
// IOUSBHostDevice. We resolve names ourselves via ioreg and rewrite any
|
||||||
|
// "port - n/a" entries with the real product name.
|
||||||
|
if (Platform.isMacOS && rawPorts.isNotEmpty) {
|
||||||
|
return _annotateMacOsPorts(rawPorts);
|
||||||
|
}
|
||||||
|
return Future.value(rawPorts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rewrites the flserial port list on macOS by substituting real USB device
|
||||||
|
/// names (obtained via [ioreg]) for the "n/a" placeholders that flserial
|
||||||
|
/// returns when it can't find the deprecated IOUSBDevice parent.
|
||||||
|
Future<List<String>> _annotateMacOsPorts(List<String> rawPorts) async {
|
||||||
|
final deviceNames = await queryMacOsUsbDeviceNames();
|
||||||
|
if (deviceNames.isEmpty) return rawPorts;
|
||||||
|
return rawPorts.map((entry) {
|
||||||
|
// entry format from fl_ports: "port - description - hardware_id"
|
||||||
|
final port = normalizeUsbPortName(entry); // e.g. /dev/cu.usbmodem1101
|
||||||
|
final knownName = deviceNames[port]; // e.g. "Nordic NRF52 DK"
|
||||||
|
if (knownName == null) return entry; // non-USB port, keep as-is
|
||||||
|
// Replace description field only; preserve hardware_id for device
|
||||||
|
// identity (used by normalizeUsbPortName).
|
||||||
|
final segments = entry.split(' - ');
|
||||||
|
final hardwareId = segments.length >= 3 ? segments.last : 'n/a';
|
||||||
|
return '$port - $knownName - $hardwareId';
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setDebugLogService(AppDebugLogService? service) {
|
||||||
|
_debugLogService = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> connect({
|
||||||
|
required String portName,
|
||||||
|
int baudRate = 115200,
|
||||||
|
}) async {
|
||||||
|
if (_status == UsbSerialStatus.connected ||
|
||||||
|
_status == UsbSerialStatus.connecting) {
|
||||||
|
throw StateError('USB serial transport is already active');
|
||||||
|
}
|
||||||
|
if (!_isSupportedPlatform) {
|
||||||
|
throw UnsupportedError('USB serial is not supported on this platform.');
|
||||||
|
}
|
||||||
|
|
||||||
|
_status = UsbSerialStatus.connecting;
|
||||||
|
var normalizedPortName = normalizeUsbPortName(portName);
|
||||||
|
_frameDecoder.reset();
|
||||||
|
|
||||||
|
if (_useAndroidUsbHost) {
|
||||||
|
try {
|
||||||
|
await _androidMethodChannel.invokeMethod<void>('connect', {
|
||||||
|
'portName': normalizedPortName,
|
||||||
|
'baudRate': baudRate,
|
||||||
|
});
|
||||||
|
_debugLogService?.info(
|
||||||
|
'USB serial opened port=$normalizedPortName on Android via USB host bridge',
|
||||||
|
tag: 'USB Serial',
|
||||||
|
);
|
||||||
|
} on PlatformException catch (error) {
|
||||||
|
_status = UsbSerialStatus.disconnected;
|
||||||
|
final msg = error.message ?? error.code;
|
||||||
|
_debugLogService?.error(
|
||||||
|
'Android connect failed: $msg',
|
||||||
|
tag: 'USB Serial',
|
||||||
|
);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// ── Hot-restart guard ─────────────────────────────────────────────────
|
||||||
|
// On hot restart Dart tears down the isolate without calling dispose().
|
||||||
|
// The NativeCallable registered by flserial's setCallback() is
|
||||||
|
// isolate-local and gets freed when the isolate dies, but the native
|
||||||
|
// SerialThread is still alive and will call it → crash.
|
||||||
|
//
|
||||||
|
// flserial uses process-global native state. Calling fl_free() kills ALL
|
||||||
|
// SerialThreads for every open port across all Dart isolates (there is
|
||||||
|
// only one in a Flutter app). Then fl_init() re-initialises the slot
|
||||||
|
// table so subsequent fl_open() calls work normally.
|
||||||
|
//
|
||||||
|
// This must happen before we register any new NativeCallable, so it must
|
||||||
|
// be the very first thing we do in the desktop branch.
|
||||||
|
try {
|
||||||
|
bindings.fl_free();
|
||||||
|
bindings.fl_init(16);
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
// On macOS, flserial lists both cu.* and tty.* device nodes.
|
||||||
|
// When a cu.* open fails with FL_ERROR_PORT_NOT_EXIST, try the tty.*
|
||||||
|
// variant as a fallback (and vice-versa) before giving up.
|
||||||
|
final candidates = _buildPortCandidates(normalizedPortName);
|
||||||
|
FlSerialException? lastError;
|
||||||
|
bool opened = false;
|
||||||
|
|
||||||
|
for (final candidate in candidates) {
|
||||||
|
// Always create a fresh FlSerial instance — a cached instance retains
|
||||||
|
// a stale flh handle from prior failed opens, which causes the native
|
||||||
|
// fl_open() to mis-route the request and report port-not-exist even
|
||||||
|
// when the device node is physically present.
|
||||||
|
final serial = _freshSerial();
|
||||||
|
serial.init();
|
||||||
|
try {
|
||||||
|
final openStatus = serial.openPort(candidate, baudRate);
|
||||||
|
if (openStatus != FlOpenStatus.open) {
|
||||||
|
final msg =
|
||||||
|
'Failed to open USB port $candidate (status: $openStatus)';
|
||||||
|
_debugLogService?.error(msg, tag: 'USB Serial');
|
||||||
|
// Not a FlSerialException — treat as terminal failure
|
||||||
|
_status = UsbSerialStatus.disconnected;
|
||||||
|
throw StateError(msg);
|
||||||
|
}
|
||||||
|
serial.setByteSize8();
|
||||||
|
serial.setBitParityNone();
|
||||||
|
serial.setStopBits1();
|
||||||
|
serial.setFlowControlNone();
|
||||||
|
serial.setRTS(false);
|
||||||
|
serial.setDTR(true);
|
||||||
|
_serial = serial;
|
||||||
|
// Update the normalized port name to whichever candidate succeeded.
|
||||||
|
normalizedPortName = candidate;
|
||||||
|
_debugLogService?.info(
|
||||||
|
'USB serial opened port=$candidate cts=${serial.getCTS()} dsr=${serial.getDSR()} dtr=true rts=false',
|
||||||
|
tag: 'USB Serial',
|
||||||
|
);
|
||||||
|
opened = true;
|
||||||
|
break;
|
||||||
|
} on FlSerialException catch (error) {
|
||||||
|
// The native fl_open() already called fl_close() on failure
|
||||||
|
// internally, so no extra cleanup is needed here for this candidate.
|
||||||
|
_debugLogService?.warn(
|
||||||
|
'Failed to open $candidate: ${error.msg} (code ${error.error})',
|
||||||
|
tag: 'USB Serial',
|
||||||
|
);
|
||||||
|
lastError = error;
|
||||||
|
// Try next candidate
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
_status = UsbSerialStatus.disconnected;
|
||||||
|
_debugLogService?.error(
|
||||||
|
'Unexpected error opening $candidate: $error\n$stackTrace',
|
||||||
|
tag: 'USB Serial',
|
||||||
|
);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opened) {
|
||||||
|
_status = UsbSerialStatus.disconnected;
|
||||||
|
final primary = candidates.first;
|
||||||
|
final msg = lastError != null
|
||||||
|
? 'Failed to open USB port $primary: ${lastError.msg} (code ${lastError.error})'
|
||||||
|
: 'Failed to open USB port $primary';
|
||||||
|
_debugLogService?.error(msg, tag: 'USB Serial');
|
||||||
|
throw StateError(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_connectedPortKey = normalizedPortName;
|
||||||
|
_connectedPortLabel = normalizedPortName;
|
||||||
|
if (_useAndroidUsbHost) {
|
||||||
|
_androidDataSubscription = _androidEventChannel
|
||||||
|
.receiveBroadcastStream()
|
||||||
|
.listen(
|
||||||
|
_handleAndroidData,
|
||||||
|
onError: _handleSerialError,
|
||||||
|
onDone: _handleSerialDone,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_dataSubscription = _serial!.onSerialData.stream.listen(
|
||||||
|
_handleSerialData,
|
||||||
|
onError: _handleSerialError,
|
||||||
|
onDone: _handleSerialDone,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_status = UsbSerialStatus.connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> write(Uint8List data) async {
|
||||||
|
if (!isConnected) {
|
||||||
|
throw StateError('USB serial port is not open');
|
||||||
|
}
|
||||||
|
final packet = wrapUsbSerialTxFrame(data);
|
||||||
|
_logFrameSummary('USB TX frame', data);
|
||||||
|
if (_useAndroidUsbHost) {
|
||||||
|
try {
|
||||||
|
await _androidMethodChannel.invokeMethod<void>('write', {
|
||||||
|
'data': packet,
|
||||||
|
});
|
||||||
|
} on PlatformException catch (error) {
|
||||||
|
throw StateError(error.message ?? error.code);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_serial!.write(packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> disconnect() async {
|
||||||
|
if (_status == UsbSerialStatus.disconnected) return;
|
||||||
|
|
||||||
|
final portLabel = _connectedPortLabel ?? _connectedPortKey;
|
||||||
|
_debugLogService?.info(
|
||||||
|
'USB disconnect starting port=${portLabel ?? 'unknown'}',
|
||||||
|
tag: 'USB Serial',
|
||||||
|
);
|
||||||
|
_status = UsbSerialStatus.disconnecting;
|
||||||
|
_connectedPortKey = null;
|
||||||
|
_connectedPortLabel = null;
|
||||||
|
_frameDecoder.reset();
|
||||||
|
|
||||||
|
if (_useAndroidUsbHost) {
|
||||||
|
await _androidDataSubscription?.cancel();
|
||||||
|
_androidDataSubscription = null;
|
||||||
|
try {
|
||||||
|
await _androidMethodChannel.invokeMethod<void>('disconnect');
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore errors while closing.
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// IMPORTANT: Close and free the native port FIRST, before cancelling the
|
||||||
|
// Dart subscription. The native SerialThread is blocked on a read(); once
|
||||||
|
// closePort() is called it unblocks and the thread exits. If we cancel
|
||||||
|
// the Dart subscription first (freeing the FFI callback pointer) and the
|
||||||
|
// thread fires one final callback before noticing the port is gone, Dart
|
||||||
|
// crashes with "Callback invoked after it has been deleted".
|
||||||
|
final serial = _serial;
|
||||||
|
_serial = null;
|
||||||
|
try {
|
||||||
|
if (serial?.isOpen() == FlOpenStatus.open) {
|
||||||
|
serial?.closePort();
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore errors while closing.
|
||||||
|
}
|
||||||
|
// Note: we do NOT call free() here; that would globally reset native
|
||||||
|
// state for all ports. The global reset is done in connect() instead,
|
||||||
|
// before the next open, which is the safer place to do it.
|
||||||
|
|
||||||
|
// Now it is safe to cancel the Dart subscription — the native thread has
|
||||||
|
// already seen the port close and will not fire any more callbacks.
|
||||||
|
await _dataSubscription?.cancel();
|
||||||
|
_dataSubscription = null;
|
||||||
|
}
|
||||||
|
_status = UsbSerialStatus.disconnected;
|
||||||
|
_debugLogService?.info(
|
||||||
|
'USB disconnect complete port=${portLabel ?? 'unknown'}',
|
||||||
|
tag: 'USB Serial',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setRequestPortLabel(String label) {
|
||||||
|
// Native implementations do not use a synthetic chooser row.
|
||||||
|
}
|
||||||
|
|
||||||
|
void setFallbackDeviceName(String label) {
|
||||||
|
// Native implementations use OS-provided device names.
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateConnectedLabel(String label) {
|
||||||
|
final trimmed = label.trim();
|
||||||
|
if (trimmed.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_connectedPortLabel = buildUsbDisplayLabel(
|
||||||
|
basePortLabel: _connectedPortKey ?? trimmed,
|
||||||
|
deviceName: trimmed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
// Synchronously close the native port so the SerialThread exits before
|
||||||
|
// the Dart isolate is torn down (e.g. on hot restart). The async
|
||||||
|
// disconnect() path via unawaited() offers no ordering guarantee — the
|
||||||
|
// isolate may die before the Future resolves, leaving the thread alive
|
||||||
|
// with a dangling NativeCallable pointer.
|
||||||
|
if (_useDesktopFlSerial) {
|
||||||
|
final serial = _serial;
|
||||||
|
try {
|
||||||
|
if (serial?.isOpen() == FlOpenStatus.open) {
|
||||||
|
serial?.closePort(); // synchronous C call — kills the SerialThread
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
// Kick off the full async teardown for anything else (subscription cancel,
|
||||||
|
// stream controller close). These are best-effort at dispose time.
|
||||||
|
unawaited(disconnect().whenComplete(_closeFrameController));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSerialData(FlSerialEventArgs event) {
|
||||||
|
try {
|
||||||
|
final bytes = event.serial.readList();
|
||||||
|
if (bytes.isNotEmpty) {
|
||||||
|
_ingestRawBytes(Uint8List.fromList(bytes));
|
||||||
|
}
|
||||||
|
} catch (error, stack) {
|
||||||
|
_addFrameError(error, stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleAndroidData(dynamic data) {
|
||||||
|
if (data is Uint8List) {
|
||||||
|
_ingestRawBytes(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data is ByteData) {
|
||||||
|
_ingestRawBytes(data.buffer.asUint8List());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_addFrameError(
|
||||||
|
StateError('Unexpected Android USB event payload: ${data.runtimeType}'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSerialError(Object error, [StackTrace? stackTrace]) {
|
||||||
|
_addFrameError(error, stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSerialDone() {
|
||||||
|
unawaited(disconnect());
|
||||||
|
}
|
||||||
|
|
||||||
|
void _ingestRawBytes(Uint8List bytes) {
|
||||||
|
for (final packet in _frameDecoder.ingest(bytes)) {
|
||||||
|
if (!packet.isRxFrame) {
|
||||||
|
_debugLogService?.info(
|
||||||
|
'USB ignored packet start=0x${packet.frameStart.toRadixString(16).padLeft(2, '0')} len=${packet.payload.length}',
|
||||||
|
tag: 'USB Serial',
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_addFrame(packet.payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addFrame(Uint8List payload) {
|
||||||
|
if (_frameController.isClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_frameController.add(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addFrameError(Object error, [StackTrace? stackTrace]) {
|
||||||
|
if (_frameController.isClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_frameController.addError(error, stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _closeFrameController() async {
|
||||||
|
if (_frameController.isClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _frameController.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _logFrameSummary(String prefix, Uint8List bytes) {
|
||||||
|
if (bytes.isEmpty) {
|
||||||
|
_debugLogService?.info('$prefix len=0', tag: 'USB Serial');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_debugLogService?.info(
|
||||||
|
'$prefix code=${bytes[0]} len=${bytes.length}',
|
||||||
|
tag: 'USB Serial',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an ordered list of port paths to try for [portName].
|
||||||
|
///
|
||||||
|
/// On macOS, USB serial devices appear as both `/dev/cu.*` (call-out, the
|
||||||
|
/// correct mode for outgoing serial connections) and `/dev/tty.*` (dial-in).
|
||||||
|
/// `flserial` may list one variant while only the other is actually openable
|
||||||
|
/// at a given moment. We prefer `cu.*` but automatically include the `tty.*`
|
||||||
|
/// sibling as a fallback, and vice-versa.
|
||||||
|
List<String> _buildPortCandidates(String normalizedPort) {
|
||||||
|
if (!Platform.isMacOS) return [normalizedPort];
|
||||||
|
const cuPrefix = '/dev/cu.';
|
||||||
|
const ttyPrefix = '/dev/tty.';
|
||||||
|
if (normalizedPort.startsWith(cuPrefix)) {
|
||||||
|
final suffix = normalizedPort.substring(cuPrefix.length);
|
||||||
|
return [normalizedPort, '$ttyPrefix$suffix'];
|
||||||
|
}
|
||||||
|
if (normalizedPort.startsWith(ttyPrefix)) {
|
||||||
|
final suffix = normalizedPort.substring(ttyPrefix.length);
|
||||||
|
return [normalizedPort, '$cuPrefix$suffix'];
|
||||||
|
}
|
||||||
|
return [normalizedPort];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum UsbSerialStatus { disconnected, connecting, connected, disconnecting }
|
||||||
@@ -0,0 +1,557 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:js_interop';
|
||||||
|
import 'dart:js_interop_unsafe';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:web/web.dart' as web;
|
||||||
|
|
||||||
|
import 'app_debug_log_service.dart';
|
||||||
|
import '../utils/usb_port_labels.dart';
|
||||||
|
import 'usb_serial_frame_codec.dart';
|
||||||
|
|
||||||
|
class UsbSerialService {
|
||||||
|
UsbSerialService();
|
||||||
|
|
||||||
|
static const Map<String, String> _knownUsbNames = <String, String>{
|
||||||
|
'2886:1667': 'Seeed Wio Tracker L1',
|
||||||
|
};
|
||||||
|
static final Map<String, String> _deviceNamesByPortKey = <String, String>{};
|
||||||
|
static final Map<String, String> _baseLabelsByPortKey = <String, String>{};
|
||||||
|
static final Map<String, JSObject> _authorizedPortsByKey =
|
||||||
|
<String, JSObject>{};
|
||||||
|
static int _nextAuthorizedPortId = 1;
|
||||||
|
|
||||||
|
final StreamController<Uint8List> _frameController =
|
||||||
|
StreamController<Uint8List>.broadcast();
|
||||||
|
final UsbSerialFrameDecoder _frameDecoder = UsbSerialFrameDecoder();
|
||||||
|
|
||||||
|
UsbSerialStatus _status = UsbSerialStatus.disconnected;
|
||||||
|
JSObject? _port;
|
||||||
|
JSObject? _reader;
|
||||||
|
JSObject? _writer;
|
||||||
|
String? _connectedPortName;
|
||||||
|
String? _connectedPortKey;
|
||||||
|
String _requestPortLabel = 'Choose USB Device';
|
||||||
|
String _fallbackDeviceName = 'Web Serial Device';
|
||||||
|
AppDebugLogService? _debugLogService;
|
||||||
|
|
||||||
|
UsbSerialStatus get status => _status;
|
||||||
|
String? get activePortKey => _connectedPortKey;
|
||||||
|
String? get activePortDisplayLabel => _connectedPortName ?? _connectedPortKey;
|
||||||
|
Stream<Uint8List> get frameStream => _frameController.stream;
|
||||||
|
bool get isConnected => _status == UsbSerialStatus.connected;
|
||||||
|
|
||||||
|
JSObject get _navigator => JSObject.fromInteropObject(web.window.navigator);
|
||||||
|
bool get _isSupported => _navigator.has('serial');
|
||||||
|
JSObject? get _serial {
|
||||||
|
if (!_isSupported) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final serial = _navigator['serial'];
|
||||||
|
return serial == null ? null : serial as JSObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> listPorts() async {
|
||||||
|
if (!_isSupported) {
|
||||||
|
return const <String>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
_resetPortCache();
|
||||||
|
final ports = await _getAuthorizedPorts();
|
||||||
|
return <String>[_requestPortListEntry, ...ports.map(_listEntryForPort)];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> connect({
|
||||||
|
required String portName,
|
||||||
|
int baudRate = 115200,
|
||||||
|
}) async {
|
||||||
|
if (_status == UsbSerialStatus.connected ||
|
||||||
|
_status == UsbSerialStatus.connecting) {
|
||||||
|
throw StateError('USB serial transport is already active');
|
||||||
|
}
|
||||||
|
if (!_isSupported) {
|
||||||
|
throw UnsupportedError('Web Serial is not supported by this browser.');
|
||||||
|
}
|
||||||
|
|
||||||
|
_status = UsbSerialStatus.connecting;
|
||||||
|
_frameDecoder.reset();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final requestedPortName = normalizeUsbPortName(portName);
|
||||||
|
_debugLogService?.info(
|
||||||
|
'Web connect: requested=$requestedPortName baud=$baudRate',
|
||||||
|
tag: 'USB Serial',
|
||||||
|
);
|
||||||
|
final selectedPortKey = requestedPortName.startsWith('web:port:')
|
||||||
|
? requestedPortName
|
||||||
|
: null;
|
||||||
|
_port = _authorizedPortsByKey[requestedPortName];
|
||||||
|
final authorizedPorts = await _getAuthorizedPorts();
|
||||||
|
_debugLogService?.info(
|
||||||
|
'Web connect: ${authorizedPorts.length} authorized port(s), cached=${_port != null}',
|
||||||
|
tag: 'USB Serial',
|
||||||
|
);
|
||||||
|
_port ??= _selectPort(authorizedPorts, requestedPortName);
|
||||||
|
|
||||||
|
_port ??= await _requestPort();
|
||||||
|
if (_port == null) {
|
||||||
|
throw StateError('No USB serial device selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
_debugLogService?.info(
|
||||||
|
'Web connect: opening port at $baudRate baud…',
|
||||||
|
tag: 'USB Serial',
|
||||||
|
);
|
||||||
|
await _openPort(_port!, baudRate);
|
||||||
|
_connectedPortKey = _cachePort(_port!, preferredKey: selectedPortKey);
|
||||||
|
_connectedPortName = _displayLabelForPort(
|
||||||
|
_port!,
|
||||||
|
portKey: _connectedPortKey,
|
||||||
|
);
|
||||||
|
_writer = _getWriter(_port!);
|
||||||
|
_reader = _getReader(_port!);
|
||||||
|
_status = UsbSerialStatus.connected;
|
||||||
|
unawaited(_pumpReads());
|
||||||
|
|
||||||
|
_debugLogService?.info(
|
||||||
|
'USB serial opened port=$_connectedPortName via Web Serial',
|
||||||
|
tag: 'USB Serial',
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
_debugLogService?.error(
|
||||||
|
'Web connect failed: $error',
|
||||||
|
tag: 'USB Serial',
|
||||||
|
);
|
||||||
|
await _cleanupFailedConnect();
|
||||||
|
_status = UsbSerialStatus.disconnected;
|
||||||
|
_connectedPortName = null;
|
||||||
|
_connectedPortKey = null;
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> write(Uint8List data) async {
|
||||||
|
if (!isConnected || _writer == null) {
|
||||||
|
throw StateError('USB serial port is not open');
|
||||||
|
}
|
||||||
|
|
||||||
|
final packet = wrapUsbSerialTxFrame(data);
|
||||||
|
_logFrameSummary('USB TX frame', data);
|
||||||
|
|
||||||
|
final promise = _writer!.callMethod<JSPromise<JSAny?>>(
|
||||||
|
'write'.toJS,
|
||||||
|
packet.toJS,
|
||||||
|
);
|
||||||
|
await promise.toDart;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> disconnect() async {
|
||||||
|
if (_status == UsbSerialStatus.disconnected) return;
|
||||||
|
|
||||||
|
final portLabel = _connectedPortName ?? _connectedPortKey;
|
||||||
|
_debugLogService?.info(
|
||||||
|
'USB disconnect starting port=${portLabel ?? 'unknown'}',
|
||||||
|
tag: 'USB Serial',
|
||||||
|
);
|
||||||
|
_status = UsbSerialStatus.disconnecting;
|
||||||
|
final reader = _reader;
|
||||||
|
final writer = _writer;
|
||||||
|
final port = _port;
|
||||||
|
|
||||||
|
_reader = null;
|
||||||
|
_writer = null;
|
||||||
|
_port = null;
|
||||||
|
_connectedPortName = null;
|
||||||
|
_connectedPortKey = null;
|
||||||
|
_frameDecoder.reset();
|
||||||
|
|
||||||
|
if (reader != null) {
|
||||||
|
try {
|
||||||
|
await reader.callMethod<JSPromise<JSAny?>>('cancel'.toJS).toDart;
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore errors while closing.
|
||||||
|
}
|
||||||
|
_releaseLock(reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (writer != null) {
|
||||||
|
_releaseLock(writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port != null) {
|
||||||
|
try {
|
||||||
|
await port.callMethod<JSPromise<JSAny?>>('close'.toJS).toDart;
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore errors while closing.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_status = UsbSerialStatus.disconnected;
|
||||||
|
_debugLogService?.info(
|
||||||
|
'USB disconnect complete port=${portLabel ?? 'unknown'}',
|
||||||
|
tag: 'USB Serial',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateConnectedLabel(String label) {
|
||||||
|
final trimmed = label.trim();
|
||||||
|
final portKey = _connectedPortKey;
|
||||||
|
if (trimmed.isEmpty || portKey == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_deviceNamesByPortKey[portKey] = trimmed;
|
||||||
|
_connectedPortName = _buildDisplayLabel(portKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setRequestPortLabel(String label) {
|
||||||
|
final trimmed = label.trim();
|
||||||
|
if (trimmed.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_requestPortLabel = trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setFallbackDeviceName(String label) {
|
||||||
|
final trimmed = label.trim();
|
||||||
|
if (trimmed.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_fallbackDeviceName = trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setDebugLogService(AppDebugLogService? service) {
|
||||||
|
_debugLogService = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
unawaited(disconnect().whenComplete(_closeFrameController));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<JSObject>> _getAuthorizedPorts() async {
|
||||||
|
final serial = _serial;
|
||||||
|
if (serial == null) {
|
||||||
|
return const <JSObject>[];
|
||||||
|
}
|
||||||
|
final result = await serial
|
||||||
|
.callMethod<JSPromise<JSAny?>>('getPorts'.toJS)
|
||||||
|
.toDart;
|
||||||
|
return _toObjectList(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<JSObject?> _requestPort() async {
|
||||||
|
final serial = _serial;
|
||||||
|
if (serial == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final result = await serial
|
||||||
|
.callMethod<JSPromise<JSAny?>>('requestPort'.toJS)
|
||||||
|
.toDart;
|
||||||
|
return result == null ? null : result as JSObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSObject? _selectPort(List<JSObject> ports, String requestedPortName) {
|
||||||
|
if (ports.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (requestedPortName.isEmpty || requestedPortName == _requestPortKey) {
|
||||||
|
return ports.first;
|
||||||
|
}
|
||||||
|
if (requestedPortName.startsWith('web:port:')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (final port in ports) {
|
||||||
|
final description = _describePort(port);
|
||||||
|
if (description == requestedPortName) {
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _openPort(JSObject port, int baudRate) {
|
||||||
|
final options = JSObject()..['baudRate'] = baudRate.toJS;
|
||||||
|
return port.callMethod<JSPromise<JSAny?>>('open'.toJS, options).toDart;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _cleanupFailedConnect() async {
|
||||||
|
final reader = _reader;
|
||||||
|
final writer = _writer;
|
||||||
|
final port = _port;
|
||||||
|
|
||||||
|
_reader = null;
|
||||||
|
_writer = null;
|
||||||
|
_port = null;
|
||||||
|
|
||||||
|
if (reader != null) {
|
||||||
|
try {
|
||||||
|
await reader.callMethod<JSPromise<JSAny?>>('cancel'.toJS).toDart;
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore cleanup errors after a failed connect.
|
||||||
|
}
|
||||||
|
_releaseLock(reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (writer != null) {
|
||||||
|
_releaseLock(writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port != null) {
|
||||||
|
try {
|
||||||
|
await port.callMethod<JSPromise<JSAny?>>('close'.toJS).toDart;
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore cleanup errors after a failed connect.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JSObject? _getReader(JSObject port) {
|
||||||
|
final readable = port.getProperty<JSAny?>('readable'.toJS);
|
||||||
|
if (readable == null) {
|
||||||
|
throw StateError('Web Serial port is not readable');
|
||||||
|
}
|
||||||
|
final readableObject = readable as JSObject;
|
||||||
|
return readableObject.callMethod<JSAny?>('getReader'.toJS) as JSObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSObject? _getWriter(JSObject port) {
|
||||||
|
final writable = port.getProperty<JSAny?>('writable'.toJS);
|
||||||
|
if (writable == null) {
|
||||||
|
throw StateError('Web Serial port is not writable');
|
||||||
|
}
|
||||||
|
final writableObject = writable as JSObject;
|
||||||
|
return writableObject.callMethod<JSAny?>('getWriter'.toJS) as JSObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pumpReads() async {
|
||||||
|
final reader = _reader;
|
||||||
|
if (reader == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (_status == UsbSerialStatus.connected &&
|
||||||
|
identical(reader, _reader)) {
|
||||||
|
final result = await reader
|
||||||
|
.callMethod<JSPromise<JSAny?>>('read'.toJS)
|
||||||
|
.toDart;
|
||||||
|
if (result == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
final resultObject = result as JSObject;
|
||||||
|
|
||||||
|
final doneValue = resultObject.getProperty<JSAny?>('done'.toJS);
|
||||||
|
final done = doneValue != null && doneValue.dartify() == true;
|
||||||
|
if (done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
final value = resultObject.getProperty<JSAny?>('value'.toJS);
|
||||||
|
final bytes = _coerceBytes(value);
|
||||||
|
if (bytes != null && bytes.isNotEmpty) {
|
||||||
|
_ingestRawBytes(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
if (_status == UsbSerialStatus.connected) {
|
||||||
|
_addFrameError(error, stackTrace);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_releaseLock(reader);
|
||||||
|
if (_status == UsbSerialStatus.connected && identical(reader, _reader)) {
|
||||||
|
_addFrameError(StateError('USB serial connection closed'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint8List? _coerceBytes(JSAny? value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
try {
|
||||||
|
return (value as JSUint8Array).toDart;
|
||||||
|
} catch (_) {
|
||||||
|
// Fall back to array-like coercion below.
|
||||||
|
}
|
||||||
|
|
||||||
|
final object = value as JSObject;
|
||||||
|
if (object.has('length')) {
|
||||||
|
final lengthValue = object.getProperty<JSAny?>('length'.toJS)?.dartify();
|
||||||
|
if (lengthValue is num) {
|
||||||
|
final length = lengthValue.toInt();
|
||||||
|
final bytes = Uint8List(length);
|
||||||
|
for (var i = 0; i < length; i++) {
|
||||||
|
final item = object.getProperty<JSAny?>(i.toString().toJS)?.dartify();
|
||||||
|
if (item is num) {
|
||||||
|
bytes[i] = item.toInt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<JSObject> _toObjectList(JSAny? value) {
|
||||||
|
if (value == null) {
|
||||||
|
return const <JSObject>[];
|
||||||
|
}
|
||||||
|
final object = value as JSObject;
|
||||||
|
if (!object.has('length')) {
|
||||||
|
return const <JSObject>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
final lengthValue = object.getProperty<JSAny?>('length'.toJS)?.dartify();
|
||||||
|
if (lengthValue is! num) {
|
||||||
|
return const <JSObject>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
final length = lengthValue.toInt();
|
||||||
|
final items = <JSObject>[];
|
||||||
|
for (var i = 0; i < length; i++) {
|
||||||
|
final item = object.getProperty<JSAny?>(i.toString().toJS);
|
||||||
|
if (item != null) {
|
||||||
|
items.add(item as JSObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _describePort(JSObject port) {
|
||||||
|
final info = _portInfo(port);
|
||||||
|
if (info == null) {
|
||||||
|
return _requestPortLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
final vendorId = info.usbVendorId;
|
||||||
|
final productId = info.usbProductId;
|
||||||
|
final hasVendor = vendorId != null;
|
||||||
|
final hasProduct = productId != null;
|
||||||
|
|
||||||
|
return describeWebUsbPort(
|
||||||
|
vendorId: hasVendor ? vendorId : null,
|
||||||
|
productId: hasProduct ? productId : null,
|
||||||
|
requestPortLabel: _requestPortLabel,
|
||||||
|
fallbackDeviceName: _fallbackDeviceName,
|
||||||
|
knownUsbNames: _knownUsbNames,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_WebPortInfo? _portInfo(JSObject port) {
|
||||||
|
try {
|
||||||
|
final info = port.callMethod<JSAny?>('getInfo'.toJS);
|
||||||
|
if (info == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final infoObject = info as JSObject;
|
||||||
|
|
||||||
|
final vendorId = infoObject
|
||||||
|
.getProperty<JSAny?>('usbVendorId'.toJS)
|
||||||
|
?.dartify();
|
||||||
|
final productId = infoObject
|
||||||
|
.getProperty<JSAny?>('usbProductId'.toJS)
|
||||||
|
?.dartify();
|
||||||
|
return _WebPortInfo(
|
||||||
|
usbVendorId: vendorId is num ? vendorId.toInt() : null,
|
||||||
|
usbProductId: productId is num ? productId.toInt() : null,
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _portKeyFor(JSObject port) {
|
||||||
|
return _cachePort(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _cachePort(JSObject port, {String? preferredKey}) {
|
||||||
|
final portKey = preferredKey ?? 'web:port:${_nextAuthorizedPortId++}';
|
||||||
|
_baseLabelsByPortKey[portKey] = _describePort(port);
|
||||||
|
_authorizedPortsByKey[portKey] = port;
|
||||||
|
return portKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _displayLabelForPort(JSObject port, {String? portKey}) =>
|
||||||
|
_buildDisplayLabel(portKey ?? _portKeyFor(port));
|
||||||
|
|
||||||
|
String _buildDisplayLabel(String portKey) {
|
||||||
|
return buildUsbDisplayLabel(
|
||||||
|
basePortLabel: _baseLabelsByPortKey[portKey] ?? portKey,
|
||||||
|
deviceName: _deviceNamesByPortKey[portKey],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _listEntryForPort(JSObject port) {
|
||||||
|
final portKey = _portKeyFor(port);
|
||||||
|
return '$portKey - ${_displayLabelForPort(port, portKey: portKey)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
String get _requestPortKey => 'web:request';
|
||||||
|
|
||||||
|
String get _requestPortListEntry => '$_requestPortKey - $_requestPortLabel';
|
||||||
|
|
||||||
|
void _resetPortCache() {
|
||||||
|
_authorizedPortsByKey.clear();
|
||||||
|
_baseLabelsByPortKey.clear();
|
||||||
|
_deviceNamesByPortKey.clear();
|
||||||
|
_nextAuthorizedPortId = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _releaseLock(JSObject resource) {
|
||||||
|
try {
|
||||||
|
resource.callMethod<JSAny?>('releaseLock'.toJS);
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore lock release failures.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _ingestRawBytes(Uint8List bytes) {
|
||||||
|
for (final packet in _frameDecoder.ingest(bytes)) {
|
||||||
|
if (!packet.isRxFrame) {
|
||||||
|
_debugLogService?.info(
|
||||||
|
'USB ignored packet start=0x${packet.frameStart.toRadixString(16).padLeft(2, '0')} len=${packet.payload.length}',
|
||||||
|
tag: 'USB Serial',
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_addFrame(packet.payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addFrame(Uint8List payload) {
|
||||||
|
if (_frameController.isClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_frameController.add(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addFrameError(Object error, [StackTrace? stackTrace]) {
|
||||||
|
if (_frameController.isClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_frameController.addError(error, stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _closeFrameController() async {
|
||||||
|
if (_frameController.isClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _frameController.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _logFrameSummary(String prefix, Uint8List bytes) {
|
||||||
|
if (bytes.isEmpty) {
|
||||||
|
_debugLogService?.info('$prefix len=0', tag: 'USB Serial');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_debugLogService?.info(
|
||||||
|
'$prefix code=${bytes[0]} len=${bytes.length}',
|
||||||
|
tag: 'USB Serial',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum UsbSerialStatus { disconnected, connecting, connected, disconnecting }
|
||||||
|
|
||||||
|
final class _WebPortInfo {
|
||||||
|
const _WebPortInfo({required this.usbVendorId, required this.usbProductId});
|
||||||
|
|
||||||
|
final int? usbVendorId;
|
||||||
|
final int? usbProductId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import '../models/discovery_contact.dart';
|
||||||
|
import 'prefs_manager.dart';
|
||||||
|
|
||||||
|
class ContactDiscoveryStore {
|
||||||
|
static const String _key = 'discovered_contacts';
|
||||||
|
|
||||||
|
Future<List<DiscoveryContact>> loadContacts() async {
|
||||||
|
final prefs = PrefsManager.instance;
|
||||||
|
final jsonStr = prefs.getString(_key);
|
||||||
|
if (jsonStr == null) return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
final jsonList = jsonDecode(jsonStr) as List<dynamic>;
|
||||||
|
return jsonList
|
||||||
|
.map((entry) => _fromJson(entry as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
} catch (_) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveContacts(List<DiscoveryContact> contacts) async {
|
||||||
|
final prefs = PrefsManager.instance;
|
||||||
|
final jsonList = contacts.map(_toJson).toList();
|
||||||
|
await prefs.setString(_key, jsonEncode(jsonList));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _toJson(DiscoveryContact contact) {
|
||||||
|
return {
|
||||||
|
'rawPacket': base64Encode(contact.rawPacket),
|
||||||
|
'publicKey': base64Encode(contact.publicKey),
|
||||||
|
'name': contact.name,
|
||||||
|
'type': contact.type,
|
||||||
|
'pathLength': contact.pathLength,
|
||||||
|
'path': base64Encode(contact.path),
|
||||||
|
'latitude': contact.latitude,
|
||||||
|
'longitude': contact.longitude,
|
||||||
|
'lastSeen': contact.lastSeen.millisecondsSinceEpoch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
DiscoveryContact _fromJson(Map<String, dynamic> json) {
|
||||||
|
final lastSeenMs = json['lastSeen'] as int? ?? 0;
|
||||||
|
return DiscoveryContact(
|
||||||
|
rawPacket: Uint8List.fromList(base64Decode(json['rawPacket'] as String)),
|
||||||
|
publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)),
|
||||||
|
name: json['name'] as String? ?? 'Unknown',
|
||||||
|
type: json['type'] as int? ?? 0,
|
||||||
|
pathLength: json['pathLength'] as int? ?? -1,
|
||||||
|
path: json['path'] != null
|
||||||
|
? Uint8List.fromList(base64Decode(json['path'] as String))
|
||||||
|
: Uint8List(0),
|
||||||
|
latitude: (json['latitude'] as num?)?.toDouble(),
|
||||||
|
longitude: (json['longitude'] as num?)?.toDouble(),
|
||||||
|
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastSeenMs),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ class ContactStore {
|
|||||||
'publicKey': base64Encode(contact.publicKey),
|
'publicKey': base64Encode(contact.publicKey),
|
||||||
'name': contact.name,
|
'name': contact.name,
|
||||||
'type': contact.type,
|
'type': contact.type,
|
||||||
|
'flags': contact.flags,
|
||||||
'pathLength': contact.pathLength,
|
'pathLength': contact.pathLength,
|
||||||
'path': base64Encode(contact.path),
|
'path': base64Encode(contact.path),
|
||||||
'pathOverride': contact.pathOverride,
|
'pathOverride': contact.pathOverride,
|
||||||
@@ -53,6 +54,7 @@ class ContactStore {
|
|||||||
publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)),
|
publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)),
|
||||||
name: json['name'] as String? ?? 'Unknown',
|
name: json['name'] as String? ?? 'Unknown',
|
||||||
type: json['type'] as int? ?? 0,
|
type: json['type'] as int? ?? 0,
|
||||||
|
flags: json['flags'] as int? ?? 0,
|
||||||
pathLength: json['pathLength'] as int? ?? -1,
|
pathLength: json['pathLength'] as int? ?? -1,
|
||||||
path: json['path'] != null
|
path: json['path'] != null
|
||||||
? Uint8List.fromList(base64Decode(json['path'] as String))
|
? Uint8List.fromList(base64Decode(json['path'] as String))
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
typedef BatteryVoltageRange = ({int minMv, int maxMv});
|
||||||
|
|
||||||
|
BatteryVoltageRange batteryVoltageRange(String chemistry) {
|
||||||
|
switch (chemistry) {
|
||||||
|
case 'lifepo4':
|
||||||
|
return (minMv: 2600, maxMv: 3650);
|
||||||
|
case 'lipo':
|
||||||
|
return (minMv: 3000, maxMv: 4200);
|
||||||
|
case 'nmc':
|
||||||
|
default:
|
||||||
|
return (minMv: 3000, maxMv: 4200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int estimateBatteryPercentFromMillivolts(int millivolts, String chemistry) {
|
||||||
|
final range = batteryVoltageRange(chemistry);
|
||||||
|
if (millivolts <= range.minMv) return 0;
|
||||||
|
if (millivolts >= range.maxMv) return 100;
|
||||||
|
return (((millivolts - range.minMv) * 100) / (range.maxMv - range.minMv))
|
||||||
|
.round();
|
||||||
|
}
|
||||||
|
|
||||||
|
int estimateBatteryPercentFromVolts(double volts, String chemistry) {
|
||||||
|
final millivolts = (volts * 1000).round();
|
||||||
|
return estimateBatteryPercentFromMillivolts(millivolts, chemistry);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export 'browser_detection_stub.dart'
|
||||||
|
if (dart.library.js_interop) 'browser_detection_web.dart';
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
class BrowserDetection {
|
||||||
|
static bool get isChrome => false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import 'package:web/web.dart' as web;
|
||||||
|
|
||||||
|
class BrowserDetection {
|
||||||
|
static bool get isChrome {
|
||||||
|
final userAgent = web.window.navigator.userAgent.toLowerCase();
|
||||||
|
final isChrome = userAgent.contains('chrome');
|
||||||
|
return isChrome;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'package:meshcore_open/models/discovery_contact.dart';
|
||||||
|
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
|
|
||||||
bool matchesContactQuery(Contact contact, String query) {
|
bool matchesContactQuery(Contact contact, String query) {
|
||||||
@@ -14,8 +16,25 @@ bool matchesContactQuery(Contact contact, String query) {
|
|||||||
return contact.publicKeyHex.toLowerCase().startsWith(hexPrefix);
|
return contact.publicKeyHex.toLowerCase().startsWith(hexPrefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool matchesDiscoveryContactQuery(DiscoveryContact contact, String query) {
|
||||||
|
final normalizedQuery = query.trim().toLowerCase();
|
||||||
|
if (normalizedQuery.isEmpty) return true;
|
||||||
|
|
||||||
|
if (contact.name.toLowerCase().contains(normalizedQuery)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final hexPrefix = _extractHexPrefix(normalizedQuery);
|
||||||
|
if (hexPrefix == null) return false;
|
||||||
|
|
||||||
|
return contact.publicKeyHex.toLowerCase().startsWith(hexPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
String? _extractHexPrefix(String query) {
|
String? _extractHexPrefix(String query) {
|
||||||
var cleaned = query;
|
var cleaned = query;
|
||||||
|
if (cleaned.startsWith('<')) {
|
||||||
|
cleaned = cleaned.substring(1).replaceAll(">", "");
|
||||||
|
}
|
||||||
if (cleaned.startsWith('0x')) {
|
if (cleaned.startsWith('0x')) {
|
||||||
cleaned = cleaned.substring(2);
|
cleaned = cleaned.substring(2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
|
import 'app_logger.dart';
|
||||||
|
|
||||||
/// Shows a confirmation dialog before disconnecting from the device.
|
/// Shows a confirmation dialog before disconnecting from the device.
|
||||||
/// Returns true if user confirmed and disconnect completed, false otherwise.
|
/// Returns true if user confirmed and disconnect completed, false otherwise.
|
||||||
@@ -28,6 +29,7 @@ Future<bool> showDisconnectDialog(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (confirmed == true) {
|
if (confirmed == true) {
|
||||||
|
appLogger.info('Disconnect confirmed from popup', tag: 'Connection');
|
||||||
await connector.disconnect();
|
await connector.disconnect();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:meshcore_open/connector/meshcore_connector.dart';
|
|||||||
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import '../utils/platform_info.dart';
|
||||||
|
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
@@ -109,6 +110,10 @@ class GpxExport {
|
|||||||
String shareText,
|
String shareText,
|
||||||
String subject,
|
String subject,
|
||||||
) async {
|
) async {
|
||||||
|
if (PlatformInfo.isWeb) {
|
||||||
|
debugPrint("GPX export is not supported on Web.");
|
||||||
|
return gpxExportNotAvailable;
|
||||||
|
}
|
||||||
if (_contacts.isEmpty) {
|
if (_contacts.isEmpty) {
|
||||||
debugPrint("No repeaters to export – nothing to share.");
|
debugPrint("No repeaters to export – nothing to share.");
|
||||||
return gpxExportNoContacts;
|
return gpxExportNoContacts;
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
/// Queries the macOS IOKit registry via [ioreg] to build a map of serial port
|
||||||
|
/// callout device paths to human-readable USB device names.
|
||||||
|
///
|
||||||
|
/// The [flserial] native library uses the deprecated [IOUSBDevice] IOKit class
|
||||||
|
/// to resolve device names, but macOS 10.15+ renamed it to [IOUSBHostDevice].
|
||||||
|
/// As a result flserial always returns "n/a" for USB product/vendor info on
|
||||||
|
/// modern macOS. This utility bypasses that limitation by invoking ioreg
|
||||||
|
/// directly and parsing its output.
|
||||||
|
///
|
||||||
|
/// Returns a Map of e.g. `"/dev/cu.usbmodem1101"` → `"Nordic NRF52 DK"`.
|
||||||
|
/// Devices without a USB product name are not included in the map.
|
||||||
|
Future<Map<String, String>> queryMacOsUsbDeviceNames() async {
|
||||||
|
assert(Platform.isMacOS);
|
||||||
|
try {
|
||||||
|
final result = await Process.run('ioreg', [
|
||||||
|
'-r',
|
||||||
|
'-c',
|
||||||
|
'IOUSBHostDevice',
|
||||||
|
'-l',
|
||||||
|
], stdoutEncoding: const SystemEncoding());
|
||||||
|
if (result.exitCode != 0) return const <String, String>{};
|
||||||
|
return _parseIoregOutput(result.stdout as String);
|
||||||
|
} catch (_) {
|
||||||
|
return const <String, String>{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> _parseIoregOutput(String output) {
|
||||||
|
final lines = output.split('\n');
|
||||||
|
final result = <String, String>{};
|
||||||
|
|
||||||
|
// We accumulate the current device block's properties.
|
||||||
|
// A new block starts at a line beginning with "+-o " which indicates a
|
||||||
|
// top-level IOUSBHostDevice entry in the ioreg tree.
|
||||||
|
String? currentVendor;
|
||||||
|
String? currentProduct;
|
||||||
|
final List<String> currentPorts = <String>[];
|
||||||
|
|
||||||
|
void flushBlock() {
|
||||||
|
if (currentPorts.isNotEmpty &&
|
||||||
|
(currentVendor != null || currentProduct != null)) {
|
||||||
|
final parts = <String>[
|
||||||
|
if (currentVendor != null && currentVendor!.isNotEmpty) currentVendor!,
|
||||||
|
if (currentProduct != null && currentProduct!.isNotEmpty)
|
||||||
|
currentProduct!,
|
||||||
|
];
|
||||||
|
final name = parts.join(' ');
|
||||||
|
for (final port in currentPorts) {
|
||||||
|
result[port] = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentVendor = null;
|
||||||
|
currentProduct = null;
|
||||||
|
currentPorts.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final line in lines) {
|
||||||
|
// A new top-level device block begins here.
|
||||||
|
if (line.startsWith('+-o ')) {
|
||||||
|
flushBlock();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// USB Product Name (appears at multiple depths in the tree, first wins)
|
||||||
|
final productMatch = _kProductName.firstMatch(line);
|
||||||
|
if (productMatch != null && currentProduct == null) {
|
||||||
|
currentProduct = productMatch.group(1)?.trim();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// USB Vendor Name
|
||||||
|
final vendorMatch = _kVendorName.firstMatch(line);
|
||||||
|
if (vendorMatch != null && currentVendor == null) {
|
||||||
|
currentVendor = vendorMatch.group(1)?.trim();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// IOCalloutDevice — the /dev/cu.xxx path our app uses
|
||||||
|
final calloutMatch = _kCalloutDevice.firstMatch(line);
|
||||||
|
if (calloutMatch != null) {
|
||||||
|
final port = calloutMatch.group(1)?.trim();
|
||||||
|
if (port != null && port.isNotEmpty) {
|
||||||
|
currentPorts.add(port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flushBlock();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
final RegExp _kProductName = RegExp(r'"USB Product Name" = "([^"]*)"');
|
||||||
|
final RegExp _kVendorName = RegExp(r'"USB Vendor Name" = "([^"]*)"');
|
||||||
|
final RegExp _kCalloutDevice = RegExp(r'"IOCalloutDevice" = "([^"]*)"');
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'dart:io' show Platform;
|
||||||
|
import 'browser_detection.dart';
|
||||||
|
|
||||||
|
/// Utility class to safely check the current platform across web and native.
|
||||||
|
///
|
||||||
|
/// Using `Platform` from `dart:io` directly on Web causes a crash.
|
||||||
|
/// This class handles the `kIsWeb` check first to avoid those crashes.
|
||||||
|
class PlatformInfo {
|
||||||
|
/// Whether the app is running in a web browser.
|
||||||
|
static bool get isWeb => kIsWeb;
|
||||||
|
|
||||||
|
/// Whether the app is running in the Chrome browser (only relevant if [isWeb] is true).
|
||||||
|
static bool get isChrome => isWeb && BrowserDetection.isChrome;
|
||||||
|
|
||||||
|
/// Whether the app is running on Android.
|
||||||
|
static bool get isAndroid => !kIsWeb && Platform.isAndroid;
|
||||||
|
|
||||||
|
/// Whether the app is running on iOS.
|
||||||
|
static bool get isIOS => !kIsWeb && Platform.isIOS;
|
||||||
|
|
||||||
|
/// Whether the app is running on macOS.
|
||||||
|
static bool get isMacOS => !kIsWeb && Platform.isMacOS;
|
||||||
|
|
||||||
|
/// Whether the app is running on Windows.
|
||||||
|
static bool get isWindows => !kIsWeb && Platform.isWindows;
|
||||||
|
|
||||||
|
/// Whether the app is running on Linux.
|
||||||
|
static bool get isLinux => !kIsWeb && Platform.isLinux;
|
||||||
|
|
||||||
|
/// Whether the app is running on a mobile platform (Android or iOS).
|
||||||
|
static bool get isMobile => isAndroid || isIOS;
|
||||||
|
|
||||||
|
/// Whether the app is running on a desktop platform (macOS, Windows, or Linux).
|
||||||
|
static bool get isDesktop => isMacOS || isWindows || isLinux;
|
||||||
|
|
||||||
|
/// Whether the current platform supports a native USB serial backend.
|
||||||
|
static bool get supportsNativeUsbSerial =>
|
||||||
|
isAndroid || isWindows || isLinux || isMacOS;
|
||||||
|
|
||||||
|
/// Whether the current browser supports the Web Serial backend.
|
||||||
|
static bool get supportsWebSerial => isWeb && isChrome;
|
||||||
|
|
||||||
|
/// Whether USB serial is expected to be available on the current platform.
|
||||||
|
static bool get supportsUsbSerial =>
|
||||||
|
supportsNativeUsbSerial || supportsWebSerial;
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
String normalizeUsbPortName(String portLabel) {
|
||||||
|
final separatorIndex = portLabel.indexOf(' - ');
|
||||||
|
final normalized = separatorIndex >= 0
|
||||||
|
? portLabel.substring(0, separatorIndex)
|
||||||
|
: portLabel;
|
||||||
|
return normalized.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a human-readable name for a serial port label.
|
||||||
|
///
|
||||||
|
/// The native flserial library encodes port info as a ` - `-separated string:
|
||||||
|
/// `"<port> - <description> - <hardware_id>"`
|
||||||
|
///
|
||||||
|
/// This function extracts the *description* field (index 1) and discards the
|
||||||
|
/// raw hardware_id, which is not user-friendly. If the description is missing
|
||||||
|
/// or unhelpful (e.g. "n/a"), it falls back to the raw port name.
|
||||||
|
String friendlyUsbPortName(String portLabel) {
|
||||||
|
final parts = portLabel.split(' - ');
|
||||||
|
if (parts.length < 2) {
|
||||||
|
return portLabel.trim();
|
||||||
|
}
|
||||||
|
// parts[0] = port name, parts[1] = description, parts[2+] = hardware id
|
||||||
|
final description = parts[1].trim();
|
||||||
|
if (description.isEmpty || description.toLowerCase() == 'n/a') {
|
||||||
|
return parts[0].trim();
|
||||||
|
}
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
String describeWebUsbPort({
|
||||||
|
required int? vendorId,
|
||||||
|
required int? productId,
|
||||||
|
String requestPortLabel = 'Choose USB Device',
|
||||||
|
String fallbackDeviceName = 'Web Serial Device',
|
||||||
|
Map<String, String> knownUsbNames = const <String, String>{},
|
||||||
|
}) {
|
||||||
|
if (vendorId == null && productId == null) {
|
||||||
|
return requestPortLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
final vendorHex = vendorId?.toRadixString(16).padLeft(4, '0').toUpperCase();
|
||||||
|
final productHex = productId?.toRadixString(16).padLeft(4, '0').toUpperCase();
|
||||||
|
final knownName = (vendorHex != null && productHex != null)
|
||||||
|
? knownUsbNames['${vendorHex.toLowerCase()}:${productHex.toLowerCase()}']
|
||||||
|
: null;
|
||||||
|
|
||||||
|
final parts = <String>[knownName ?? fallbackDeviceName];
|
||||||
|
if (vendorHex != null) {
|
||||||
|
parts.add('VID:$vendorHex');
|
||||||
|
}
|
||||||
|
if (productHex != null) {
|
||||||
|
parts.add('PID:$productHex');
|
||||||
|
}
|
||||||
|
return '${parts.first} (${parts.skip(1).join(' ')})';
|
||||||
|
}
|
||||||
|
|
||||||
|
String buildUsbDisplayLabel({
|
||||||
|
required String basePortLabel,
|
||||||
|
String? deviceName,
|
||||||
|
}) {
|
||||||
|
final trimmedName = deviceName?.trim() ?? '';
|
||||||
|
if (trimmedName.isEmpty) {
|
||||||
|
return basePortLabel;
|
||||||
|
}
|
||||||
|
return '$basePortLabel - $trimmedName';
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AdaptiveAppBarTitle extends StatelessWidget {
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
const AdaptiveAppBarTitle(this.text, {super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) => SizedBox(
|
||||||
|
width: constraints.maxWidth,
|
||||||
|
child: FittedBox(fit: BoxFit.scaleDown, child: Text(text, maxLines: 1)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user