mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-08-05 23:42:57 +10:00
Compare commits
211 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c8a15538e | |||
| 68eeefa04e | |||
| 2da8995d0b | |||
| 1c376b0056 | |||
| da70d5fc08 | |||
| f63bc4b787 | |||
| 9b1f1e1994 | |||
| 5f475fce4d | |||
| 0228c38621 | |||
| fc7283f076 | |||
| 7eff1df6e2 | |||
| 58252b8a40 | |||
| 630606acdc | |||
| bd030153c1 | |||
| 5140ff383d | |||
| dc57f9b9c0 | |||
| 53cd3f4461 | |||
| 35e296f1cd | |||
| 532401cc94 | |||
| 5321974cbb | |||
| 7c16dde989 | |||
| 9a75c912af | |||
| 767dc1164e | |||
| dbefb0b5f4 | |||
| 4f609f160f | |||
| e313bea3fc | |||
| 77be2b8e6f | |||
| c81c3efe7c | |||
| cac0cc15eb | |||
| 1392c2d00f | |||
| cb63b48b78 | |||
| 4ad4a93a20 | |||
| 4962a48e64 | |||
| b88e5e647a | |||
| 87d11c2e6b | |||
| 7b3c099736 | |||
| 11cb14a925 | |||
| d2df2b0bed | |||
| 723bf7293c | |||
| 53caec3e14 | |||
| 3c440ca3d4 | |||
| 8797d8ffde | |||
| faba120823 | |||
| be690c8194 | |||
| 64d75dde45 | |||
| 9199aab7f7 | |||
| 60e8ee0130 | |||
| 6dfb7a4b69 | |||
| 28a423e0a8 | |||
| 3593cfa843 | |||
| dc85e7a41c | |||
| 9265daaf16 | |||
| 4b744184c2 | |||
| 64698e0be6 | |||
| 3dd9037be3 | |||
| 566e3aadf8 | |||
| 06a906f4f7 | |||
| 054a84031e | |||
| fffcff3b74 | |||
| b336aedbc5 | |||
| 2ee2358ecc | |||
| 86e9b7fe01 | |||
| 24fa78741b | |||
| 79a45c527b | |||
| 8b280b37be | |||
| fa4da979af | |||
| 91608ff09e | |||
| 71f59d23df | |||
| e90742be25 | |||
| db935a7454 | |||
| 1ad5db27ca | |||
| 81758adc61 | |||
| c81791cf1e | |||
| 1fba5312a2 | |||
| 2f770bbd53 | |||
| 9db79e9d40 | |||
| 1913a5aa11 | |||
| 929c1c3d28 | |||
| 7a2bb20bf7 | |||
| a1b77bb29b | |||
| 4eecfc92dc | |||
| 90c8cf5f3e | |||
| 06fa176367 | |||
| e4285774a0 | |||
| b2da695102 | |||
| e1327a93c7 | |||
| 421bc71bb7 | |||
| fef73b7b62 | |||
| 84ec139ce6 | |||
| b748b96237 | |||
| c2671ac2ae | |||
| 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 | |||
| a777236cd9 | |||
| 7740698cde | |||
| c8f93f9902 | |||
| c34be44950 | |||
| 549fc62632 | |||
| 53d073d8f2 | |||
| 4975b5366e | |||
| c284e571b0 | |||
| a1ee0789a6 | |||
| 40ac95e8e6 | |||
| 377f1df445 | |||
| 9865a03c53 | |||
| a5555bd606 | |||
| 1b4d31a36e | |||
| 8e07440114 | |||
| 71129bdf4d | |||
| ab05cf8b3e | |||
| 452e5337f0 | |||
| 6ac987e7cf | |||
| 5522f9a236 | |||
| b4f79c1aae | |||
| b08defcff4 | |||
| 5676cbd84e | |||
| cf8f01128b | |||
| b5e47ce44f | |||
| 7b2f75047c | |||
| 6d63e49938 |
@@ -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
|
||||||
@@ -58,6 +58,7 @@ secrets.dart
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.AppleDouble
|
.AppleDouble
|
||||||
.LSOverride
|
.LSOverride
|
||||||
|
macos/Flutter/GeneratedPluginRegistrant.swift
|
||||||
|
|
||||||
# iOS
|
# iOS
|
||||||
**/ios/Pods/
|
**/ios/Pods/
|
||||||
@@ -83,3 +84,6 @@ keystore.properties
|
|||||||
# IDE
|
# IDE
|
||||||
.vscode/launch.json
|
.vscode/launch.json
|
||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
|
|
||||||
|
# Cloudflare Wrangler
|
||||||
|
.wrangler
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
6.2.4
|
||||||
@@ -51,7 +51,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
|||||||
|
|
||||||
### Device Management
|
### Device Management
|
||||||
|
|
||||||
- **BLE Connection**: Scan and connect to MeshCore devices via Bluetooth
|
- **BLE, USB, TCP Connection**: Scan and connect to MeshCore devices via Bluetooth, USB or TCP
|
||||||
- **Device Settings**: Configure radio parameters, power settings, and network options
|
- **Device Settings**: Configure radio parameters, power settings, and network options
|
||||||
- **Battery Monitoring**: Real-time battery status with chemistry-specific voltage curves
|
- **Battery Monitoring**: Real-time battery status with chemistry-specific voltage curves
|
||||||
- **Firmware Updates**: Over-the-air firmware updates via BLE (coming soon)
|
- **Firmware Updates**: Over-the-air firmware updates via BLE (coming soon)
|
||||||
@@ -75,10 +75,16 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
|||||||
|
|
||||||
### Platform Support
|
### Platform Support
|
||||||
|
|
||||||
- ✅ **Android**: Full support (API 21+)
|
| Feature | Android (API 21+) | iOS (12+) | Linux | Windows | macOS | Web |
|
||||||
- ✅ **iOS**: Full support (iOS 12+)
|
|--------------------|:-----------------:|:---------:|:-----:|:-------:|:-----:|:---------------------------------:|
|
||||||
- 🚧 **Desktop**: Limited support (macOS/Linux/Windows)
|
| BLE companion | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
- 🚧 **Web**: Under construction (Chrome)
|
| USB companion | ✅ | 🚧 | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| TCP companion | ✅ | 🚧 | ✅ | ✅ | ✅ | ❌<br>(requires websocket bridge) |
|
||||||
|
| Core Functionality | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Mesh Network | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Map & Location | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Device Management | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Repeater Hub | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
@@ -189,6 +195,7 @@ Messages are transmitted as binary frames using a custom protocol optimized for
|
|||||||
### App Settings
|
### App Settings
|
||||||
|
|
||||||
- **Theme**: System default, light, or dark mode
|
- **Theme**: System default, light, or dark mode
|
||||||
|
- **Language**: Use one of 15 languages (English, Chinese, French, Spanish, Portuguese, German, Dutch, Polish, Swedish, Italian, Slovak, Slovene, Bulgarian, Russian, Ukrainian)
|
||||||
- **Notifications**: Configurable for messages, channels, and node advertisements
|
- **Notifications**: Configurable for messages, channels, and node advertisements
|
||||||
- **Battery Chemistry**: Support for NMC, LiFePO4, and LiPo battery types
|
- **Battery Chemistry**: Support for NMC, LiFePO4, and LiPo battery types
|
||||||
- **Message Retry**: Automatic retry with configurable path clearing
|
- **Message Retry**: Automatic retry with configurable path clearing
|
||||||
@@ -231,6 +238,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
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ if (keystorePropertiesFile.exists()) {
|
|||||||
android {
|
android {
|
||||||
namespace = "com.meshcore.meshcore_open"
|
namespace = "com.meshcore.meshcore_open"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = "29.0.14206865"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
@@ -0,0 +1,30 @@
|
|||||||
|
# MeshCore Open - Feature Documentation
|
||||||
|
|
||||||
|
MeshCore Open is an open-source Flutter client for MeshCore LoRa mesh networking devices. This documentation covers every user-facing feature, how to access it, and what it does.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Scanner & Connection](scanner-and-connection.md) - BLE scanning, USB serial, and TCP connection
|
||||||
|
2. [Navigation](navigation.md) - App flow, device screen, and quick-switch navigation
|
||||||
|
3. [Contacts](contacts.md) - Contact management, groups, discovery, and sharing
|
||||||
|
4. [Chat & Messaging](chat-and-messaging.md) - Direct messages, message status, reactions, and retries
|
||||||
|
5. [Channels](channels.md) - Broadcast channels, communities, and channel chat
|
||||||
|
6. [Map & Location](map-and-location.md) - Node map, path tracing, line-of-sight, and offline caching
|
||||||
|
7. [Settings](settings.md) - Device settings, app settings, radio configuration, and exports
|
||||||
|
8. [Notifications](notifications.md) - System notifications, unread badges, and notification preferences
|
||||||
|
9. [Repeater Management](repeater-management.md) - Repeater hub, status, CLI, telemetry, and neighbors
|
||||||
|
10. [Additional Features](additional-features.md) - GIF picker, localization, debug logs, SMAZ compression, and more
|
||||||
|
11. [BLE Protocol & Data Layer](ble-protocol.md) - Technical reference for the communication protocol and data architecture
|
||||||
|
|
||||||
|
## App Overview
|
||||||
|
|
||||||
|
MeshCore Open connects to MeshCore LoRa mesh radios over BLE, USB, or TCP. Once connected, users can:
|
||||||
|
|
||||||
|
- **Chat** with other mesh nodes via encrypted direct messages
|
||||||
|
- **Broadcast** on shared channels (public, hashtag, private, or community-scoped)
|
||||||
|
- **View nodes on a map** with GPS locations, predicted positions, and path traces
|
||||||
|
- **Manage repeaters** with CLI access, telemetry, neighbor info, and settings
|
||||||
|
- **Share contacts** via `meshcore://` URIs and QR codes
|
||||||
|
- **Configure radio settings** including frequency, power, bandwidth, and spreading factor
|
||||||
|
- **Cache offline maps** for use without internet connectivity
|
||||||
|
- **Analyze line-of-sight** between nodes with terrain elevation profiles
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
# Additional Features
|
||||||
|
|
||||||
|
## GIF Picker (Giphy Integration)
|
||||||
|
|
||||||
|
### How to Access
|
||||||
|
In any chat screen (direct or channel), tap the GIF button in the message input bar.
|
||||||
|
|
||||||
|
### What the User Sees
|
||||||
|
A bottom sheet with a search field and a grid of GIF thumbnails.
|
||||||
|
|
||||||
|
### Key Interactions
|
||||||
|
- On open, loads trending GIFs (G-rated, 25 results)
|
||||||
|
- Type to search and press the keyboard submit button (search triggers on submit, not on each keystroke). Clearing the search field reloads trending GIFs
|
||||||
|
- On network/API errors, a "Retry" button is shown in-place
|
||||||
|
- Tap a GIF to select it — the chat input shows an inline preview with an X button to dismiss
|
||||||
|
- Send the message to transmit the GIF reference (`g:<giphy-id>`)
|
||||||
|
- Recipients see the GIF rendered inline via Giphy CDN
|
||||||
|
- "Powered by Giphy" attribution is always shown at the bottom of the picker
|
||||||
|
- The bottom sheet occupies 70% of screen height
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Localization / Multi-Language Support
|
||||||
|
|
||||||
|
### How to Access
|
||||||
|
App Settings → Appearance → Language
|
||||||
|
|
||||||
|
### Supported Languages (15)
|
||||||
|
English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
- All UI strings go through Flutter's ARB localization system
|
||||||
|
- Language can follow the system locale or be explicitly overridden
|
||||||
|
- Changes take effect immediately
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Discovered Contacts Screen
|
||||||
|
|
||||||
|
### How to Access
|
||||||
|
From Contacts screen → overflow menu → "Discovered Contacts"
|
||||||
|
|
||||||
|
### What the User Sees
|
||||||
|
A list of nodes heard passively over the air but not yet added as contacts. Each shows:
|
||||||
|
- Color-coded avatar (by type)
|
||||||
|
- Name
|
||||||
|
- Short public key
|
||||||
|
- Last-seen time
|
||||||
|
|
||||||
|
### Key Interactions
|
||||||
|
- Search bar with debounced filtering
|
||||||
|
- Sort by last seen or name; filter by type
|
||||||
|
- **Tap**: Import the contact (adds to your contact list)
|
||||||
|
- **Long-press**: Add Contact, Copy `meshcore://` URI to clipboard, or Delete
|
||||||
|
- Overflow menu → "Delete All" (with confirmation)
|
||||||
|
- Already-known contacts and your own node are filtered out
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SMAZ Compression
|
||||||
|
|
||||||
|
### What It Is
|
||||||
|
An optional per-contact and per-channel text compression feature using the SMAZ algorithm (optimized for short English text).
|
||||||
|
|
||||||
|
### How to Enable
|
||||||
|
- **Per contact**: Chat screen → info button → toggle "SMAZ compression"
|
||||||
|
- **Per channel**: Long-press channel → Edit → toggle "SMAZ compression"
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
- When enabled, compression is applied using a "compress only if smaller" strategy — the message is only transmitted compressed if the encoded result is actually shorter than the original. Otherwise, the original text is sent uncompressed
|
||||||
|
- Compressed messages are transmitted with a `s:` prefix followed by base64-encoded data
|
||||||
|
- Recipients using MeshCore Open will decompress automatically. **Recipients using other software** that is not SMAZ-aware will see garbled `s:...` text
|
||||||
|
- The codec operates on ASCII. Non-ASCII / non-English text generally does not benefit from compression and may even expand. Best suited for short English messages
|
||||||
|
- Disabled by default
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Community QR Scanner
|
||||||
|
|
||||||
|
### How to Access
|
||||||
|
From Channels screen → "+" FAB → "Scan Community QR"
|
||||||
|
|
||||||
|
### What the User Sees
|
||||||
|
A live QR scanner view with instruction text overlay.
|
||||||
|
|
||||||
|
### Key Interactions
|
||||||
|
- Scan a community QR code shared by another member
|
||||||
|
- On valid scan: confirmation dialog showing community name and ID
|
||||||
|
- Option to "Add public channel to device" on join
|
||||||
|
- If already a member: shows an "Already a member" dialog
|
||||||
|
- Invalid QR: shows an orange error snackbar
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Channel Message Path Viewing
|
||||||
|
|
||||||
|
### How to Access
|
||||||
|
In a channel chat, tap a message bubble (mobile) or use the "Path" action (desktop).
|
||||||
|
|
||||||
|
### What the User Sees
|
||||||
|
- Summary card: sender, time, repeat count, path type, observed hops
|
||||||
|
- "Other Observed Paths" section (if multiple paths detected)
|
||||||
|
- "Repeater Hops" section listing each hop with hex prefix, resolved name, and GPS coordinates
|
||||||
|
|
||||||
|
### Actions
|
||||||
|
- **Radar icon**: Opens path trace map for live trace
|
||||||
|
- **Map icon**: Opens a map with hop markers and polyline
|
||||||
|
- **Path dropdown**: Switch between observed path variants (if multiple)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debug Logging
|
||||||
|
|
||||||
|
### BLE Debug Log
|
||||||
|
**Access**: Settings → BLE Debug Log
|
||||||
|
|
||||||
|
Two views:
|
||||||
|
- **Frames**: Each BLE frame with direction, description, hex preview, timestamp. Long-press to copy hex.
|
||||||
|
- **Raw Log RX**: Decoded LoRa packets with route type, payload type, path bytes, and summary.
|
||||||
|
|
||||||
|
### App Debug Log
|
||||||
|
**Access**: Settings → App Debug Log (must be enabled first in App Settings → Debug)
|
||||||
|
|
||||||
|
Structured log entries with level (Info/Warning/Error), tag, message, and timestamp.
|
||||||
|
|
||||||
|
Both logs support copy-all and clear operations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chrome Required Screen
|
||||||
|
|
||||||
|
### When It Appears
|
||||||
|
Automatically shown on web platforms when a non-Chromium browser is detected.
|
||||||
|
|
||||||
|
### What the User Sees
|
||||||
|
A full-screen informational page explaining that Web Bluetooth requires a Chromium-based browser. No interactive elements — purely informational.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Path History Service
|
||||||
|
|
||||||
|
### What It Does (Background Service)
|
||||||
|
Maintains an in-memory LRU cache of up to 50 contacts, each with up to 100 route history entries, tracking:
|
||||||
|
- Hop count and trip time
|
||||||
|
- Success/failure counts and route weights
|
||||||
|
- Flood vs. direct discovery
|
||||||
|
|
||||||
|
### Path Scoring
|
||||||
|
Paths are scored using a weighted formula: reliability (45%), route weight (20%), latency (25%), and freshness (10%). These weights are internal and not user-configurable. Paths whose weight drops to zero or below are automatically deleted. Flood deliveries that receive an ACK give a weight boost (+0.5) to the specific return path.
|
||||||
|
|
||||||
|
Used internally for:
|
||||||
|
- **Auto route rotation**: Cycles through known paths using configurable weights on retries, with a diversity window to avoid re-using recently tried paths
|
||||||
|
- **Path selection**: Picks the best-scored path for each retry attempt
|
||||||
|
- **Flood statistics**: Tracks flood vs. direct discovery ratios
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Message Retry Service
|
||||||
|
|
||||||
|
### What It Does (Background Service)
|
||||||
|
Handles reliable delivery of outgoing direct messages:
|
||||||
|
1. Assigns a UUID and sends immediately. Only one message per contact can be in-flight at a time (avoids overflowing the firmware's 8-entry ACK table); subsequent messages are queued
|
||||||
|
2. Listens for ACK frames matched via SHA-256 hash of `[timestamp][attempt][text][sender_pubkey]`
|
||||||
|
3. On timeout, retries with exponential backoff: `1000 × 2^retryCount` ms (1s, 2s, 4s, 8s...)
|
||||||
|
4. Each retry may use a different path (via path history diversity window)
|
||||||
|
5. After max retries: marks failed but keeps a **30-second grace window** during which a late ACK can still resolve the message to "delivered". Optionally clears the contact's path
|
||||||
|
6. Reports RTT and path data for quality learning
|
||||||
|
7. Maintains an ACK hash history (last 50 entries) to handle duplicate ACKs
|
||||||
|
|
||||||
|
### Configurable Settings (App Settings → Messaging)
|
||||||
|
- Max retries (2–10, default 5)
|
||||||
|
- Clear path on max retry (on/off)
|
||||||
|
- Auto route rotation with weight parameters
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Timeout Prediction (ML)
|
||||||
|
|
||||||
|
### What It Does (Background Service)
|
||||||
|
An ML-based service that predicts expected delivery timeouts:
|
||||||
|
- Collects delivery observations (path length, message size, time since last RX, delivery time) in a sliding window of up to 100 observations (oldest evicted first)
|
||||||
|
- Requires **10 minimum observations** before first training. After that, retrains every 5 new observations
|
||||||
|
- Applies a **1.5x safety margin** to raw predictions (the actual timeout issued is 1.5× the model's predicted delivery time)
|
||||||
|
- Features with zero variance are automatically excluded from training
|
||||||
|
- Blends per-contact statistics with ML predictions
|
||||||
|
- Falls back to `3000 + 3000 × pathLength` ms when insufficient data
|
||||||
|
- Observations are persisted to storage via a 2-second debounced timer (observations within 2s of app termination may be lost)
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
# BLE Protocol & Data Layer
|
||||||
|
|
||||||
|
This is a technical reference for the communication protocol and data architecture.
|
||||||
|
|
||||||
|
## Transport Layer
|
||||||
|
|
||||||
|
The app supports three transports, all sharing the same command/response protocol:
|
||||||
|
|
||||||
|
| Transport | Method | Implementation |
|
||||||
|
|---|---|---|
|
||||||
|
| Bluetooth LE | Nordic UART Service (NUS) GATT | `flutter_blue_plus` |
|
||||||
|
| USB Serial | Packet-framed serial | `MeshCoreUsbManager` |
|
||||||
|
| TCP | Packet-framed socket | `MeshCoreTcpConnector` |
|
||||||
|
|
||||||
|
### BLE (Nordic UART Service)
|
||||||
|
|
||||||
|
- **Service UUID**: `6e400001-b5a3-f393-e0a9-e50e24dcca9e`
|
||||||
|
- **RX Characteristic** (write to device): `6e400002-b5a3-f393-e0a9-e50e24dcca9e`
|
||||||
|
- **TX Characteristic** (notify from device): `6e400003-b5a3-f393-e0a9-e50e24dcca9e`
|
||||||
|
|
||||||
|
Raw `Uint8List` payloads are written directly to the RX characteristic. Writes use "write without response" if supported, falling back to "write with response".
|
||||||
|
|
||||||
|
### USB and TCP Framing
|
||||||
|
|
||||||
|
Both use a lightweight packet framing codec:
|
||||||
|
|
||||||
|
```
|
||||||
|
TX (host → device): [0x3C][len_lo][len_hi][payload...]
|
||||||
|
RX (device → host): [0x3E][len_lo][len_hi][payload...]
|
||||||
|
```
|
||||||
|
|
||||||
|
- Frame start: `0x3C` (`<`) for outgoing, `0x3E` (`>`) for incoming
|
||||||
|
- Length: 2-byte little-endian, payload only
|
||||||
|
- Max payload: 172 bytes
|
||||||
|
- TCP: `tcpNoDelay: true` (Nagle disabled), writes serialized to prevent interleaving
|
||||||
|
- USB: 10ms post-write delay between frames
|
||||||
|
|
||||||
|
## Connection State Machine
|
||||||
|
|
||||||
|
```
|
||||||
|
enum MeshCoreConnectionState {
|
||||||
|
disconnected,
|
||||||
|
scanning,
|
||||||
|
connecting,
|
||||||
|
connected,
|
||||||
|
disconnecting,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## BLE Connection Lifecycle
|
||||||
|
|
||||||
|
1. **Scan** with keyword filters `["MeshCore-", "Whisper-"]`
|
||||||
|
2. **Connect** with 15-second timeout
|
||||||
|
3. **Request MTU** 185 bytes (non-web only)
|
||||||
|
4. **Discover services** and locate NUS
|
||||||
|
5. **Enable TX notifications** (up to 3 attempts on native)
|
||||||
|
6. **Subscribe** to TX characteristic for incoming frames
|
||||||
|
7. **Initial sync**: device info query, time sync, channel sync
|
||||||
|
|
||||||
|
## Auto-Reconnect (BLE Only)
|
||||||
|
|
||||||
|
On unexpected disconnection, auto-reconnect with exponential backoff:
|
||||||
|
- Delays: 1s, 2s, 4s, 8s, 16s, 30s, 30s...
|
||||||
|
- Resets on successful connection
|
||||||
|
- Disabled for manual disconnects
|
||||||
|
- Not available for USB or TCP
|
||||||
|
|
||||||
|
## Protocol Constants
|
||||||
|
|
||||||
|
| Constant | Value | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| Max frame size | 172 bytes | BLE/USB/TCP payload limit |
|
||||||
|
| Public key size | 32 bytes | Ed25519 public key |
|
||||||
|
| Max path size | 64 bytes | Maximum path data |
|
||||||
|
| Max name size | 32 bytes | Maximum node name |
|
||||||
|
| Max text payload | 160 bytes | Firmware `MAX_TEXT_LEN` |
|
||||||
|
| App protocol version | 3 | Sent in device query |
|
||||||
|
| Contact frame size | 148 bytes | Fixed-size contact record |
|
||||||
|
|
||||||
|
## Command Codes (App → Device)
|
||||||
|
|
||||||
|
| Code | Name | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| 1 | CMD_APP_START | Announce app connection |
|
||||||
|
| 2 | CMD_SEND_TXT_MSG | Send direct text message |
|
||||||
|
| 3 | CMD_SEND_CHANNEL_TXT_MSG | Send channel text message |
|
||||||
|
| 4 | CMD_GET_CONTACTS | Request contact list |
|
||||||
|
| 5 | CMD_GET_DEVICE_TIME | Query device clock |
|
||||||
|
| 6 | CMD_SET_DEVICE_TIME | Set device clock |
|
||||||
|
| 7 | CMD_SEND_SELF_ADVERT | Broadcast own advertisement |
|
||||||
|
| 8 | CMD_SET_ADVERT_NAME | Set node name |
|
||||||
|
| 9 | CMD_ADD_UPDATE_CONTACT | Add or update a contact |
|
||||||
|
| 10 | CMD_SYNC_NEXT_MESSAGE | Request next queued message |
|
||||||
|
| 11 | CMD_SET_RADIO_PARAMS | Set radio parameters |
|
||||||
|
| 12 | CMD_SET_RADIO_TX_POWER | Set TX power |
|
||||||
|
| 13 | CMD_RESET_PATH | Reset contact path |
|
||||||
|
| 14 | CMD_SET_ADVERT_LATLON | Set advertised location |
|
||||||
|
| 15 | CMD_REMOVE_CONTACT | Remove a contact |
|
||||||
|
| 16 | CMD_SHARE_CONTACT | Share contact to mesh |
|
||||||
|
| 17 | CMD_EXPORT_CONTACT | Export contact as bytes |
|
||||||
|
| 18 | CMD_IMPORT_CONTACT | Import contact from bytes |
|
||||||
|
| 19 | CMD_REBOOT | Reboot device |
|
||||||
|
| 20 | CMD_GET_BATT_AND_STORAGE | Query battery and storage |
|
||||||
|
| 22 | CMD_DEVICE_QUERY | Query device info |
|
||||||
|
| 26 | CMD_SEND_LOGIN | Login to repeater/room |
|
||||||
|
| 27 | CMD_SEND_STATUS_REQ | Request repeater status |
|
||||||
|
| 30 | CMD_GET_CONTACT_BY_KEY | Get contact by public key |
|
||||||
|
| 31 | CMD_GET_CHANNEL | Get channel definition |
|
||||||
|
| 32 | CMD_SET_CHANNEL | Set channel name and PSK |
|
||||||
|
| 36 | CMD_SEND_TRACE_PATH | Request path trace |
|
||||||
|
| 38 | CMD_SET_OTHER_PARAMS | Set misc parameters |
|
||||||
|
| 39 | CMD_GET_TELEMETRY_REQ | Request sensor telemetry |
|
||||||
|
| 40 | CMD_GET_CUSTOM_VAR | Get custom variables |
|
||||||
|
| 41 | CMD_SET_CUSTOM_VAR | Set a custom variable |
|
||||||
|
| 50 | CMD_SEND_BINARY_REQ | Send binary request |
|
||||||
|
| 57 | CMD_SEND_ANON_REQ | Send anonymous request |
|
||||||
|
| 58 | CMD_SET_AUTO_ADD_CONFIG | Set auto-add configuration |
|
||||||
|
| 59 | CMD_GET_AUTO_ADD_CONFIG | Get auto-add configuration |
|
||||||
|
|
||||||
|
## Response / Push Codes (Device → App)
|
||||||
|
|
||||||
|
| Code | Name | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| 0 | RESP_CODE_OK | Generic success |
|
||||||
|
| 1 | RESP_CODE_ERR | Generic error |
|
||||||
|
| 2 | RESP_CODE_CONTACTS_START | Contact list begins |
|
||||||
|
| 3 | RESP_CODE_CONTACT | Single contact data |
|
||||||
|
| 4 | RESP_CODE_END_OF_CONTACTS | Contact list complete |
|
||||||
|
| 5 | RESP_CODE_SELF_INFO | Device self-info response |
|
||||||
|
| 6 | RESP_CODE_SENT | Message transmitted; carries `[1]=is_flood, [2–5]=ack_hash, [6–9]=estimated_timeout_ms` |
|
||||||
|
| 7 | RESP_CODE_CONTACT_MSG_RECV | Incoming direct message (v2) |
|
||||||
|
| 8 | RESP_CODE_CHANNEL_MSG_RECV | Incoming channel message (v2) |
|
||||||
|
| 10 | RESP_CODE_NO_MORE_MESSAGES | No more queued messages |
|
||||||
|
| 11 | RESP_CODE_EXPORT_CONTACT | Exported contact data |
|
||||||
|
| 9 | RESP_CODE_CURR_TIME | Current device time |
|
||||||
|
| 12 | RESP_CODE_BATT_AND_STORAGE | Battery mV (uint16 LE) + storage used/total (uint32 LE each) |
|
||||||
|
| 13 | RESP_CODE_DEVICE_INFO | Firmware info |
|
||||||
|
| 16 | RESP_CODE_CONTACT_MSG_RECV_V3 | Incoming direct message (v3) |
|
||||||
|
| 17 | RESP_CODE_CHANNEL_MSG_RECV_V3 | Incoming channel message (v3) |
|
||||||
|
| 18 | RESP_CODE_CHANNEL_INFO | Channel definition |
|
||||||
|
| 21 | RESP_CODE_CUSTOM_VARS | Custom variables |
|
||||||
|
| 25 | RESP_CODE_AUTO_ADD_CONFIG | Auto-add flags |
|
||||||
|
| 0x80 | PUSH_CODE_ADVERT | Known contact re-seen |
|
||||||
|
| 0x81 | PUSH_CODE_PATH_UPDATED | Better path found; carries the 32-byte public key of the updated contact |
|
||||||
|
| 0x82 | PUSH_CODE_SEND_CONFIRMED | Delivery ACK from remote; carries ACK hash (4 bytes) + trip time (4 bytes) |
|
||||||
|
| 0x83 | PUSH_CODE_MSG_WAITING | Offline messages queued |
|
||||||
|
| 0x85 | PUSH_CODE_LOGIN_SUCCESS | Repeater/room login succeeded |
|
||||||
|
| 0x86 | PUSH_CODE_LOGIN_FAIL | Repeater/room login failed |
|
||||||
|
| 0x87 | PUSH_CODE_STATUS_RESPONSE | Repeater status response |
|
||||||
|
| 0x88 | PUSH_CODE_LOG_RX_DATA | Radio RX data with SNR (int8, units 1/4 dB), RSSI, and raw radio packet |
|
||||||
|
| 0x89 | PUSH_CODE_TRACE_DATA | Path trace result |
|
||||||
|
| 0x8A | PUSH_CODE_NEW_ADVERT | New node discovered |
|
||||||
|
| 0x8B | PUSH_CODE_TELEMETRY_RESPONSE | Sensor telemetry data |
|
||||||
|
| 0x8C | PUSH_CODE_BINARY_RESPONSE | Binary data response |
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### Contact
|
||||||
|
32-byte public key (primary identity), name, type (chat/repeater/room/sensor), flags, path data, GPS coordinates, last-seen timestamp. Parsed from 148-byte firmware frames with this layout:
|
||||||
|
|
||||||
|
```
|
||||||
|
[0] = resp_code
|
||||||
|
[1–32] = public key (32 bytes)
|
||||||
|
[33] = type (1=chat, 2=repeater, 3=room, 4=sensor)
|
||||||
|
[34] = flags (bit 0 = favorite)
|
||||||
|
[35] = path_length
|
||||||
|
[36–99] = path (64 bytes)
|
||||||
|
[100–131] = name (32 bytes, null-padded)
|
||||||
|
[132–135] = timestamp (uint32 LE)
|
||||||
|
[136–139] = latitude (int32 LE, × 1e-6 degrees)
|
||||||
|
[140–143] = longitude (int32 LE, × 1e-6 degrees)
|
||||||
|
[144–147] = last_modified (uint32 LE)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Message (Direct)
|
||||||
|
Sender key, text, timestamp, outgoing flag, status (pending/sent/delivered/failed), message ID (UUID), retry count, ACK hash, trip time, path data, reactions.
|
||||||
|
|
||||||
|
### Channel Message
|
||||||
|
Sender name, text, timestamp, status (pending/sent/failed), repeater hops, path variants, channel index, reactions, reply threading fields.
|
||||||
|
|
||||||
|
### Channel
|
||||||
|
Index (0–7), name, 16-byte PSK, unread count. PSK derivation methods for hashtag (SHA-256) and community (HMAC-SHA256) channels.
|
||||||
|
|
||||||
|
### Community
|
||||||
|
UUID, name, 32-byte secret, hashtag channel list. Shared via QR code.
|
||||||
|
|
||||||
|
## Persistence
|
||||||
|
|
||||||
|
All data is stored via `SharedPreferences` (JSON-serialized). No SQLite or other database.
|
||||||
|
|
||||||
|
| Data | Storage Key Pattern | Scope |
|
||||||
|
|---|---|---|
|
||||||
|
| Contacts | `contacts<pubKey10>` | Per device identity |
|
||||||
|
| Messages | `messages_<pubKey10><contactKey>` | Per device + contact |
|
||||||
|
| Channel Messages | `channel_messages_<pubKey10><index>` | Per device + channel |
|
||||||
|
| Channels | `channels<pubKey10>` | Per device identity |
|
||||||
|
| Channel Order | `channel_order_<pubKey10>` | Per device identity |
|
||||||
|
| Contact Groups | `contact_groups<pubKey10>` | Per device identity |
|
||||||
|
| Communities | `communities_v1<pubKey10>` | Per device identity |
|
||||||
|
| Unread Counts | `contact_unread_count<pubKey10>` | Per device identity |
|
||||||
|
| Discovered Contacts | `discovered_contacts` | Global |
|
||||||
|
| App Settings | `app_settings` | Global |
|
||||||
|
| Path History | `path_history_<contactKey>` | Per contact |
|
||||||
|
|
||||||
|
## Auto-Add Configuration Bitmask
|
||||||
|
|
||||||
|
Used by `CMD_SET_AUTO_ADD_CONFIG` (58) and `RESP_CODE_AUTO_ADD_CONFIG` (25):
|
||||||
|
|
||||||
|
| Bit | Flag | Description |
|
||||||
|
|-----|------|-------------|
|
||||||
|
| 0 | 0x01 | Overwrite oldest contact when list is full |
|
||||||
|
| 1 | 0x02 | Auto-add chat users |
|
||||||
|
| 2 | 0x04 | Auto-add repeaters |
|
||||||
|
| 3 | 0x08 | Auto-add room servers |
|
||||||
|
| 4 | 0x10 | Auto-add sensors |
|
||||||
|
|
||||||
|
## Radio Packet Payload Types
|
||||||
|
|
||||||
|
Seen inside `PUSH_CODE_LOG_RX_DATA` raw packets:
|
||||||
|
|
||||||
|
| Code | Type |
|
||||||
|
|------|------|
|
||||||
|
| 0x00 | REQ (request) |
|
||||||
|
| 0x01 | RESPONSE |
|
||||||
|
| 0x02 | TXTMSG (text message) |
|
||||||
|
| 0x03 | ACK |
|
||||||
|
| 0x04 | ADVERT |
|
||||||
|
| 0x05 | GRPTXT (group/channel text) |
|
||||||
|
| 0x06 | GRPDATA (group data) |
|
||||||
|
| 0x07 | ANONREQ (anonymous request) |
|
||||||
|
| 0x08 | PATH |
|
||||||
|
| 0x09 | TRACE |
|
||||||
|
| 0x0A | MULTIPART |
|
||||||
|
| 0x0B | CONTROL |
|
||||||
|
| 0x0F | RAW_CUSTOM |
|
||||||
|
|
||||||
|
## State Management
|
||||||
|
|
||||||
|
Uses Flutter `Provider` with `ChangeNotifier`. The central state holder is `MeshCoreConnector`, which owns all in-memory collections and fires debounced (50ms) `notifyListeners()` to update the UI. In-memory conversations are windowed to 200 messages per contact; older messages remain on disk and are loaded on demand.
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
1. Raw frames arrive over BLE/USB/TCP
|
||||||
|
2. First byte is parsed as response/push code
|
||||||
|
3. Appropriate model factory (`fromFrame()`) parses the data
|
||||||
|
4. In-memory collections are updated
|
||||||
|
5. Storage stores are persisted (async)
|
||||||
|
6. `notifyListeners()` triggers UI rebuilds
|
||||||
|
7. Screens read current state via getters
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
# Channels
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Channels are broadcast group-chat spaces secured by a 16-byte pre-shared key (PSK). Any device with the same channel index and PSK will receive and decrypt channel messages. Unlike direct messages, channel messages are broadcast to the entire mesh.
|
||||||
|
|
||||||
|
Up to 8 channels (indices 0–7) can be active simultaneously on one device.
|
||||||
|
|
||||||
|
## How to Access
|
||||||
|
|
||||||
|
QuickSwitchBar tab 1 (middle) from any main screen.
|
||||||
|
|
||||||
|
## Channel Types
|
||||||
|
|
||||||
|
| Type | Icon | Color | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Public | Globe | Green | Fixed well-known PSK; any device can join |
|
||||||
|
| Hashtag | Hash tag | Blue | PSK derived from the hashtag name via SHA-256; discoverable by convention |
|
||||||
|
| Private | Lock | Blue | Random PSK; requires out-of-band sharing of the 32-hex key |
|
||||||
|
| Community | Groups/Tag | Purple | PSK derived via HMAC-SHA256 from a community's shared secret |
|
||||||
|
|
||||||
|
## Channels List Screen
|
||||||
|
|
||||||
|
### What the User Sees
|
||||||
|
|
||||||
|
- **Search bar** with live text filtering (300ms debounce)
|
||||||
|
- **Sort/filter button**
|
||||||
|
- **Scrollable list of channel cards**, each showing:
|
||||||
|
- Type icon with color coding (purple badge overlay for community channels)
|
||||||
|
- Channel name (or "Channel N" if unnamed)
|
||||||
|
- Subtitle: "Public channel", "Hashtag channel", "Private channel", or "Community channel - {name}"
|
||||||
|
- Unread badge (if messages are unread)
|
||||||
|
- Drag handle (when manual sort is active)
|
||||||
|
- **"+" FAB** to add a new channel
|
||||||
|
- **Overflow menu**: Disconnect, Manage Communities (only shown when at least one community exists), Settings
|
||||||
|
|
||||||
|
If no channels exist, an empty state with an "Add Public Channel" shortcut is shown. If a search produces no results, a separate "no results" empty state with a search-off icon is shown.
|
||||||
|
|
||||||
|
Pull-to-refresh (swipe down) forces a re-fetch of channels from the device firmware.
|
||||||
|
|
||||||
|
### Sorting Options
|
||||||
|
|
||||||
|
- **Manual** (default): Drag-and-drop reordering, persisted (drag handles are hidden when a search query is active)
|
||||||
|
- **A–Z**: Alphabetical
|
||||||
|
- **Latest messages**: Most recent first
|
||||||
|
- **Unread**: Most unread first
|
||||||
|
|
||||||
|
## Adding a Channel
|
||||||
|
|
||||||
|
Tap the "+" FAB to open a dialog with six options:
|
||||||
|
|
||||||
|
1. **Create Private Channel** — Enter a name (max 31 characters); a random PSK is generated
|
||||||
|
2. **Join Private Channel** — Enter a name and a 32-hex PSK (non-hex characters like spaces and dashes are silently stripped, so pasted keys with formatting are accepted)
|
||||||
|
3. **Join Public Channel** — One tap; uses the well-known public PSK (only shown if no public channel exists)
|
||||||
|
4. **Join Hashtag Channel** — Enter a hashtag name; PSK is derived from the name. If communities exist, choose between regular hashtag (SHA-256) or community hashtag (HMAC)
|
||||||
|
5. **Scan Community QR** — Opens QR scanner to join a community
|
||||||
|
6. **Create Community** — Enter a name; generates a random 32-byte secret; optionally adds a community public channel; shows QR code for sharing
|
||||||
|
|
||||||
|
## Channel Actions (Long-Press / Right-Click)
|
||||||
|
|
||||||
|
| Action | Description |
|
||||||
|
|---|---|
|
||||||
|
| Edit | Change name, PSK (with a dice icon to generate a random PSK), or SMAZ compression toggle (compresses outgoing messages to allow longer text within the byte limit) |
|
||||||
|
| Mute / Unmute | Toggle push notification suppression for this channel |
|
||||||
|
| Delete | Remove the channel from the device (confirmation required) |
|
||||||
|
|
||||||
|
## Channel Chat
|
||||||
|
|
||||||
|
Tap a channel card to open the channel chat screen.
|
||||||
|
|
||||||
|
### App Bar
|
||||||
|
|
||||||
|
- Type icon (public/private/hashtag)
|
||||||
|
- Channel name
|
||||||
|
- Subtitle: "{type} - {N} unread"
|
||||||
|
|
||||||
|
### Message Display
|
||||||
|
|
||||||
|
- Reverse-scrolling list (newest at bottom)
|
||||||
|
- **Incoming messages**: Colored avatar with sender's initial (or first emoji if name starts with one; color is deterministic from sender name hash), sender name in primary color, message bubble
|
||||||
|
- **Outgoing messages**: Primary container color bubble with a small status icon: pending (clock), sent (checkmark), or failed (red error circle)
|
||||||
|
- Automatic older-message loading on scroll-to-top
|
||||||
|
- Jump-to-bottom button when scrolled up
|
||||||
|
- **Pinch-to-zoom**: Two-finger zoom (0.8x–1.8x) and double-tap to reset text size
|
||||||
|
- **Message tracing mode** (when enabled in App Settings): Each bubble additionally shows path prefix bytes (`via XX,YY,...`), a timestamp, and a repeat count icon
|
||||||
|
|
||||||
|
### Message Types in Chat
|
||||||
|
|
||||||
|
- **Plain text** with linkified URLs
|
||||||
|
- **GIFs** (`g:{gifId}`) rendered inline via Giphy CDN
|
||||||
|
- **Location pins** (`m:{lat},{lon}|{label}|`) shown as tappable location cards
|
||||||
|
- **Reactions** displayed as emoji pills below target messages
|
||||||
|
|
||||||
|
### Replies (Channel Chat Only)
|
||||||
|
|
||||||
|
- **Mobile**: Swipe an **incoming** message left to trigger reply (with haptic feedback). You cannot swipe your own outgoing messages. Swipe reply is not available on desktop.
|
||||||
|
- **All platforms**: Long-press → "Reply"
|
||||||
|
- Reply banner appears above the input bar with the quoted message (tap X to cancel)
|
||||||
|
- Sent replies are prefixed `@[{senderName}] {text}`
|
||||||
|
- Received replies show a bordered quote block inside the bubble; tapping scrolls to the original. Reply previews render GIF thumbnails and location pin icons, not just text.
|
||||||
|
|
||||||
|
### Message Path Viewing
|
||||||
|
|
||||||
|
- **Mobile**: Tap a message bubble to view its routing path
|
||||||
|
- **Desktop**: Long-press/right-click → "Path" (tapping the bubble does nothing on desktop)
|
||||||
|
- Opens the Channel Message Path Screen (see [Additional Features](additional-features.md))
|
||||||
|
|
||||||
|
### Context Actions (Long-Press / Right-Click)
|
||||||
|
|
||||||
|
| Action | Availability | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| Reply | All messages | Triggers reply mode |
|
||||||
|
| Path | Desktop only | Opens message path view |
|
||||||
|
| Add Reaction | Incoming messages only | Opens emoji picker (cannot react to your own messages) |
|
||||||
|
| Copy | All messages | Copies text to clipboard |
|
||||||
|
| Delete | All messages | Removes locally (not from mesh) |
|
||||||
|
|
||||||
|
### Message Path Viewing
|
||||||
|
|
||||||
|
Tap a message bubble to open the Channel Message Path Screen, which shows:
|
||||||
|
- Each hop in the path as a visual chain
|
||||||
|
- Known contacts identified by name at each hop
|
||||||
|
- Observed vs. declared hop counts
|
||||||
|
- Alternative path variants (if received via multiple paths)
|
||||||
|
- Map view buttons for geographic path visualization
|
||||||
|
|
||||||
|
## Communities
|
||||||
|
|
||||||
|
Communities are a layer above channels that provide a private namespace.
|
||||||
|
|
||||||
|
### What is a Community?
|
||||||
|
|
||||||
|
A community has a name and a 32-byte random secret. Channel PSKs are derived from this secret:
|
||||||
|
- **Public channel**: `HMAC-SHA256(secret, "channel:v1:__public__")[:16]`
|
||||||
|
- **Hashtag channel**: `HMAC-SHA256(secret, "channel:v1:{hashtag}")[:16]`
|
||||||
|
|
||||||
|
Outsiders who don't know the secret cannot discover or join community channels.
|
||||||
|
|
||||||
|
### Sharing a Community
|
||||||
|
|
||||||
|
Communities are shared via QR codes containing a JSON payload:
|
||||||
|
```json
|
||||||
|
{"v": 1, "type": "meshcore_community", "name": "...", "k": "<base64url-secret>"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Managing Communities
|
||||||
|
|
||||||
|
From the channels screen overflow menu → "Manage Communities". Opens a draggable scrollable sheet (resizable 30–90% of screen height):
|
||||||
|
|
||||||
|
- Each community shows its name and a short community ID (first 8 hex characters)
|
||||||
|
- **Tap a community** to directly show its QR code for sharing
|
||||||
|
- **Popup menu** per community:
|
||||||
|
- **Show QR** — displays the QR code for sharing with new members
|
||||||
|
- **Delete** — removes the community locally and deletes all associated device channels (confirmation dialog warns how many channels will be removed)
|
||||||
|
|
||||||
|
## How Channels Differ from Direct Messages
|
||||||
|
|
||||||
|
| Aspect | Channels | Direct Messages |
|
||||||
|
|---|---|---|
|
||||||
|
| Addressing | Broadcast to all nodes with matching PSK | Point-to-point to a specific contact |
|
||||||
|
| Encryption | Shared PSK (symmetric) | Contact's public key (asymmetric) |
|
||||||
|
| Sender identity | Plain text prefix in payload | Verified via public key |
|
||||||
|
| Replies | Supported (swipe or long-press) | Not supported |
|
||||||
|
| Retry mechanism | No automatic retry | Exponential backoff with path rotation |
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
# Chat & Messaging
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The app supports two chat modes:
|
||||||
|
- **Direct messages**: Encrypted point-to-point messages to individual contacts
|
||||||
|
- **Channel messages**: Broadcast messages to shared channels (see [Channels](channels.md))
|
||||||
|
|
||||||
|
This page covers direct messaging. For channel chat, see the Channels documentation.
|
||||||
|
|
||||||
|
## How to Access
|
||||||
|
|
||||||
|
From the Contacts screen, tap any Chat-type contact to open the ChatScreen.
|
||||||
|
|
||||||
|
## Chat Screen Layout
|
||||||
|
|
||||||
|
### App Bar
|
||||||
|
|
||||||
|
- **Title**: Contact name
|
||||||
|
- **Subtitle**: Current routing path label (e.g., "2 hops", "flood (auto)", "direct (forced)") and unread count. Tapping the subtitle shows the full path details.
|
||||||
|
- **Action buttons**:
|
||||||
|
- **Routing mode** (waves icon): Switch between Auto, Direct, and Flood routing
|
||||||
|
- **Path management** (timeline icon): View recent paths with hop count, round-trip time, age, and success count. Paths are color-coded by direct repeater (green/yellow/red/blue for ranked repeaters, grey for unknown). Tap a path to activate it (the device verifies and confirms via snackbar), long-press to view full path details, set custom paths, or force flood mode. A warning banner appears when history reaches 100 entries.
|
||||||
|
- **Info** (info icon): Contact info dialog showing type, path, GPS coordinates, public key, and SMAZ compression toggle
|
||||||
|
|
||||||
|
### Message List
|
||||||
|
|
||||||
|
- Scrollable list with newest messages at the bottom
|
||||||
|
- **Outgoing messages**: Right-aligned, primary color background. **Failed messages** change to a red-toned error container background
|
||||||
|
- **Incoming messages**: Left-aligned, grey background with a colored avatar (initial letter or first emoji of sender name; color is deterministic from a hash of the sender name)
|
||||||
|
- Bubble width capped at 65% of screen width
|
||||||
|
- Hyperlinks rendered as tappable green underlined text
|
||||||
|
- **Pinch-to-zoom**: Two-finger zoom (0.8x–1.8x) and double-tap to reset
|
||||||
|
- **Jump to bottom**: Floating button appears when scrolled away from the bottom
|
||||||
|
- **Lazy loading**: Scrolling to top loads older messages from storage
|
||||||
|
|
||||||
|
### Input Bar
|
||||||
|
|
||||||
|
- **GIF button** (left): Opens GIF picker bottom sheet
|
||||||
|
- **Text field** (center): Auto-capitalization, enforces UTF-8 byte limit in real-time
|
||||||
|
- **Send button** (right): Submits the message
|
||||||
|
- On desktop: Enter/Numpad Enter also submits
|
||||||
|
- When a GIF is selected, the text field shows an inline GIF preview with a dismiss button
|
||||||
|
|
||||||
|
## Message Types
|
||||||
|
|
||||||
|
| Type | Wire Format | Display |
|
||||||
|
|---|---|---|
|
||||||
|
| Plain text | Raw UTF-8 string | Inline text with link detection |
|
||||||
|
| GIF | `g:<giphy-id>` | Inline GIF image from Giphy CDN |
|
||||||
|
| Location pin | `m:<lat>,<lon>\|<label>\|...` | Location icon + label; tap to open map |
|
||||||
|
| Reaction | `r:<hash>:<emoji-index>` | Applied to target message as emoji pill |
|
||||||
|
|
||||||
|
## Message Status
|
||||||
|
|
||||||
|
Outgoing messages display a status indicator:
|
||||||
|
|
||||||
|
| Status | Icon | Meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| Pending | Grey double-check | Queued, waiting for device to transmit (visually identical to Sent) |
|
||||||
|
| Sent | Grey double-check | Device confirmed transmission (visually identical to Pending) |
|
||||||
|
| Delivered | Green double-check | Remote node acknowledged receipt |
|
||||||
|
| Failed | Red X | All retries exhausted |
|
||||||
|
|
||||||
|
### Message Tracing Mode
|
||||||
|
|
||||||
|
When enabled in App Settings, additional metadata appears inside each bubble:
|
||||||
|
- Timestamp (HH:MM)
|
||||||
|
- Retry count (e.g., "Retry 2 of 4")
|
||||||
|
- Status icon
|
||||||
|
- Round-trip time in seconds (if delivered)
|
||||||
|
|
||||||
|
## Message Length Limits
|
||||||
|
|
||||||
|
- **Direct messages**: 156 bytes (UTF-8) — enforced in real-time by the input formatter
|
||||||
|
- **Channel messages**: 160 minus sender name length minus 2 bytes for the `"<name>: "` prefix
|
||||||
|
- Over-length paste shows a snackbar error
|
||||||
|
|
||||||
|
## Send Queue
|
||||||
|
|
||||||
|
Only one message per contact can be in-flight at a time (to avoid overflowing the firmware's 8-entry ACK table). If you send multiple messages rapidly, they are queued and sent sequentially — each waits for the previous one to be delivered, fail, or exhaust retries before transmitting.
|
||||||
|
|
||||||
|
## Retry Mechanism
|
||||||
|
|
||||||
|
When a direct message is sent:
|
||||||
|
|
||||||
|
1. The app computes an expected ACK hash: `SHA256([timestamp][attempt][text][selfPubKey])[0:4]` — matching the firmware's hash calculation. If SMAZ compression is enabled, the compressed text (not the original) is hashed
|
||||||
|
2. On device acknowledgment (`RESP_CODE_SENT`), the message transitions to "sent" and a timeout timer starts
|
||||||
|
3. **Timeout duration**: Preferably from the ML timeout prediction service; otherwise `3000 + 3000 × path_length` milliseconds (15000ms for flood)
|
||||||
|
4. On timeout, the message is retried with **exponential backoff**: `1000 × 2^retryCount` ms (1s, 2s, 4s, 8s, 16s...)
|
||||||
|
5. **Max retries**: Configurable (default 5, range 2–10)
|
||||||
|
6. After max retries, the message is marked "failed" — but a **30-second grace window** remains during which a late ACK can still resolve the message to "delivered"
|
||||||
|
7. If **Clear Path on Max Retry** is enabled (App Settings), the contact's stored routing path is automatically cleared when max retries are exhausted
|
||||||
|
8. **Auto route rotation**: When enabled (and no manual path override is set), the retry service uses a diversity window to avoid re-using recently tried paths, cycling through known routes on each attempt
|
||||||
|
|
||||||
|
### Manual Retry
|
||||||
|
|
||||||
|
Long-press a failed message → "Retry" to re-send using the current routing settings.
|
||||||
|
|
||||||
|
## Reactions
|
||||||
|
|
||||||
|
Add emoji reactions to incoming messages (not your own):
|
||||||
|
|
||||||
|
1. Long-press (or right-click on desktop) a message
|
||||||
|
2. Select "Add reaction" from the context menu
|
||||||
|
3. Choose from quick emojis (thumbs up, heart, laugh, party, clap, fire) or browse the full emoji picker
|
||||||
|
4. Reactions appear as pills below the message bubble with emoji and count
|
||||||
|
5. Pending reactions show at 50% opacity with a spinner
|
||||||
|
6. Failed reactions show a red retry icon (tap to retry)
|
||||||
|
|
||||||
|
## Context Actions (Long-Press / Right-Click)
|
||||||
|
|
||||||
|
| Action | Availability | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| Add reaction | Incoming messages only | Opens emoji picker |
|
||||||
|
| View path | Mobile: tap bubble directly; Desktop: long-press/right-click menu | Shows message routing path |
|
||||||
|
| Copy | All messages | Copies text to clipboard |
|
||||||
|
| Delete | All messages | Removes locally (not from mesh) |
|
||||||
|
| Retry | Failed outgoing messages | Re-sends the message |
|
||||||
|
| Open chat with sender | Room server chats | Opens 1:1 chat with the message sender |
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
# Contacts
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Contacts screen is the primary hub for managing mesh nodes your radio has a relationship with. A "contact" is any node whose cryptographic advertisement has been received — it can be a chat user, repeater, room server, or sensor.
|
||||||
|
|
||||||
|
## How to Access
|
||||||
|
|
||||||
|
- Automatically shown after connecting to a device
|
||||||
|
- QuickSwitchBar tab 0 (leftmost) from Channels or Map screens
|
||||||
|
- Back navigation from Chat or Settings screens
|
||||||
|
|
||||||
|
## Contact Types
|
||||||
|
|
||||||
|
| Type | Avatar Color | Icon | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Chat | Blue | Chat bubble | Another user's mesh radio |
|
||||||
|
| Repeater | Orange | Cell tower | A mesh repeater/relay node |
|
||||||
|
| Room | Purple | Group | A room server for group chat |
|
||||||
|
| Sensor | Green | Sensors | A sensor device |
|
||||||
|
|
||||||
|
## Contact List
|
||||||
|
|
||||||
|
Each contact is displayed as a list tile showing:
|
||||||
|
|
||||||
|
- **Avatar**: Color-coded circle with type icon (or first emoji of the contact's name if it starts with one)
|
||||||
|
- **Name**: Contact name (single line)
|
||||||
|
- **Path label**: "Direct", "N hops", or "Flood" (with forced variants if a path override is active)
|
||||||
|
- **Public key**: Shortened hex format `<XXXXXXXX...XXXXXXXX>`
|
||||||
|
- **Unread badge**: Red pill with count (if unread messages exist)
|
||||||
|
- **Last seen**: Relative timestamp ("Now", "5 mins ago", "2 hours ago", "3 days ago"). For chat contacts, this shows whichever is more recent: the last advertisement time or the last message time
|
||||||
|
- **Favorite star**: Amber star icon if favorited
|
||||||
|
- **Location pin**: Grey pin icon if the contact has GPS coordinates
|
||||||
|
|
||||||
|
Pull-to-refresh re-fetches the full contact list from the device.
|
||||||
|
|
||||||
|
## Search and Filter
|
||||||
|
|
||||||
|
A toolbar at the top provides:
|
||||||
|
|
||||||
|
**Search**: Matches contact name (case-insensitive) or public key hex prefix. Debounced at 300ms.
|
||||||
|
|
||||||
|
**Sort options**:
|
||||||
|
- Latest Messages (by most recent message)
|
||||||
|
- Heard Recently (by last seen / last message)
|
||||||
|
- A–Z (alphabetical)
|
||||||
|
|
||||||
|
**Filter options**:
|
||||||
|
- All, Favorites, Users, Repeaters, Room Servers, Unread Only
|
||||||
|
|
||||||
|
## Contact Groups
|
||||||
|
|
||||||
|
Groups are a client-side organizational feature for grouping contacts.
|
||||||
|
|
||||||
|
- **Create a group**: Tap the group dropdown → "+" icon → enter name → select members → Save
|
||||||
|
- **Edit a group**: Group dropdown → pencil icon next to the group
|
||||||
|
- **Delete a group**: Group dropdown → trash icon next to the group
|
||||||
|
- **Filter by group**: Select a group from the dropdown to show only its members
|
||||||
|
|
||||||
|
Groups are stored per radio identity (scoped by public key).
|
||||||
|
|
||||||
|
**Validation rules**: Group names cannot be empty, cannot be "all" (reserved, case-insensitive), and must be unique (case-insensitive). The group creation dialog includes a built-in search field to filter contacts when selecting members. Creating a new group automatically selects it as the active filter.
|
||||||
|
|
||||||
|
## Tap Actions
|
||||||
|
|
||||||
|
| Contact Type | Action on Tap |
|
||||||
|
|---|---|
|
||||||
|
| Chat / Sensor | Opens ChatScreen for direct messaging |
|
||||||
|
| Repeater | Shows password login dialog → opens RepeaterHubScreen |
|
||||||
|
| Room | Shows password login dialog → opens ChatScreen for room chat |
|
||||||
|
|
||||||
|
## Long-Press / Right-Click Menu
|
||||||
|
|
||||||
|
| Action | Availability | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| Path Trace / Ping | Repeaters, Rooms (always); Chat if `pathLength > 0` | Opens PathTraceMapScreen. Label shows "Ping" when no path bytes are known, "Path Trace" otherwise |
|
||||||
|
| Manage Repeater | Repeaters only | Login dialog → RepeaterHubScreen |
|
||||||
|
| Room Login | Rooms only | Login dialog → ChatScreen |
|
||||||
|
| Room Management | Rooms only | Login dialog → RepeaterHubScreen (management mode) |
|
||||||
|
| Open Chat | Chat/Sensor | Same as single tap |
|
||||||
|
| Add/Remove Favorite | All types | Toggles the favorite flag |
|
||||||
|
| Share Contact | All types | Copies `meshcore://<hex>` URI to clipboard |
|
||||||
|
| Share Contact Zero-Hop | All types | Broadcasts the contact's advertisement one hop |
|
||||||
|
| Delete Contact | All types | Confirmation dialog → removes from device and clears messages |
|
||||||
|
|
||||||
|
## App Bar Menus
|
||||||
|
|
||||||
|
The Contacts screen has **two separate popup menus** in the app bar:
|
||||||
|
|
||||||
|
**Antenna icon menu** (contact sharing):
|
||||||
|
- Zero-Hop Advert — broadcasts your advertisement to immediately adjacent nodes
|
||||||
|
- Flood Advert — broadcasts across the full mesh network
|
||||||
|
- Copy Advert to Clipboard — copies your `meshcore://<hex>` URI for sharing externally
|
||||||
|
- Add Contact from Clipboard — reads a `meshcore://<hex>` URI from clipboard and imports it
|
||||||
|
|
||||||
|
**Three-dot overflow menu**:
|
||||||
|
- Disconnect — disconnects from the device
|
||||||
|
- Discovered Contacts — opens the DiscoveryScreen
|
||||||
|
- Settings — opens the Settings screen
|
||||||
|
|
||||||
|
## Adding Contacts
|
||||||
|
|
||||||
|
### Automatic (Passive)
|
||||||
|
When the radio hears an advertisement, the contact appears automatically if auto-add is enabled for that type (configurable in Settings → Contact Settings).
|
||||||
|
|
||||||
|
### Import from Clipboard
|
||||||
|
Antenna menu → "Add Contact from Clipboard". Reads a `meshcore://<hex>` URI from clipboard and imports it to the device.
|
||||||
|
|
||||||
|
### Import from Discovered Contacts
|
||||||
|
Overflow menu → "Discovered Contacts". Shows nodes heard passively that haven't been added yet. Tap to immediately import (no confirmation dialog), or long-press for more options (Add, Copy URI, Delete). The Discovery screen has its own search bar, type filters (Users, Repeaters, Rooms, Favorites), and sort options (Last Seen, A-Z). An overflow "Delete All" option clears all discovered contacts.
|
||||||
|
|
||||||
|
## Contact Sharing Format
|
||||||
|
|
||||||
|
Contacts are shared using the `meshcore://` URI scheme:
|
||||||
|
```
|
||||||
|
meshcore://<hex-encoded-advertisement-packet>
|
||||||
|
```
|
||||||
|
This contains the node's public key and metadata. Paste it into another MeshCore app to import.
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
# Map & Location
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Map feature is a full-featured node-location visualization and radio-planning tool built on OpenStreetMap tiles. It is one of the three primary views accessible from the QuickSwitchBar.
|
||||||
|
|
||||||
|
## How to Access
|
||||||
|
|
||||||
|
- **QuickSwitchBar tab 2** (rightmost) from Contacts or Channels
|
||||||
|
- **Deep-link from a chat message**: Tapping a shared location pin in a chat opens the map centered on that pin
|
||||||
|
- **Settings → Offline Map Cache**: Opens the tile cache management screen
|
||||||
|
|
||||||
|
## What the Map Displays
|
||||||
|
|
||||||
|
### Self Location (Teal Circle)
|
||||||
|
Your own node's position, obtained from the device firmware. Displayed as a teal `person_pin_circle` icon. Only appears if the device has GPS data or a manually-set location.
|
||||||
|
|
||||||
|
### Contact / Node Markers (Color-Coded)
|
||||||
|
All contacts with known GPS coordinates are plotted:
|
||||||
|
|
||||||
|
| Type | Color | Icon |
|
||||||
|
|---|---|---|
|
||||||
|
| Chat user | Blue | Person |
|
||||||
|
| Repeater | Green | Router |
|
||||||
|
| Room | Purple | Meeting room |
|
||||||
|
| Sensor | Orange | Sensors |
|
||||||
|
|
||||||
|
Node name labels appear automatically at zoom level 12 and above.
|
||||||
|
|
||||||
|
### Shared Map Pins (Flag Icons)
|
||||||
|
Location pins shared in chat messages are displayed as flags:
|
||||||
|
- **Blue flag**: From a direct message
|
||||||
|
- **Purple flag**: From a private channel
|
||||||
|
- **Orange flag**: From a public channel
|
||||||
|
|
||||||
|
Tap a pin to see its info. Options to "Hide" (session only) or "Remove" (persistent).
|
||||||
|
|
||||||
|
### Predicted / Guessed Locations (Semi-Transparent)
|
||||||
|
|
||||||
|
Many contacts on the mesh don't have GPS hardware, so the map has no explicit coordinates for them. Instead of leaving these contacts invisible, the app **infers an approximate position** by analyzing the repeater path the contact's messages travel through. These inferred positions are displayed as semi-transparent markers with a `not_listed_location` icon, visually distinct from confirmed-location markers.
|
||||||
|
|
||||||
|
#### Why guessed locations exist
|
||||||
|
|
||||||
|
In a mesh network, every message hops through one or more repeaters on its way to the destination. Each repeater in the path is identified by the first byte of its public key. If any of those repeaters have a known GPS location (because they advertise it), then a contact that routes through those repeaters must be somewhere within radio range of them. By combining the positions of multiple repeaters a contact is known to use, the app can triangulate a rough area where the contact is likely located.
|
||||||
|
|
||||||
|
#### How the algorithm works
|
||||||
|
|
||||||
|
1. **Build a repeater index**: The app collects all known contacts of type Repeater that have a valid GPS position and indexes them by the first byte of their public key.
|
||||||
|
|
||||||
|
2. **Collect anchor points**: For each contact that lacks GPS, the app looks at the **last-hop byte** of the contact's current path and also searches the `PathHistoryService` for recent paths. Each last-hop byte that matches a located repeater becomes an "anchor point" — a GPS coordinate the contact is likely near.
|
||||||
|
|
||||||
|
3. **Resolve ambiguity**: If multiple repeaters share the same first public-key byte (a hash collision), that byte is discarded as ambiguous. Only unambiguous one-to-one matches are kept.
|
||||||
|
|
||||||
|
4. **Filter geometric inconsistencies**: Two anchor points separated by more than `2 × maxRangeKm` (the estimated LoRa radio range, computed from the current frequency, bandwidth, spreading factor, and TX power using a free-space path loss model) cannot both be in range of the same node. Outlier anchors are removed to keep only a geometrically consistent set.
|
||||||
|
|
||||||
|
5. **Compute the estimated position**:
|
||||||
|
- **Single anchor**: The contact is placed on a small circle (330m radius) around the repeater. The angle on the circle is deterministic — derived from an FNV-1a hash of the contact's public key — so the same contact always appears at the same offset, preventing markers from stacking on top of each other.
|
||||||
|
- **Two or more anchors**: The position is the average (centroid) of all anchor coordinates, with a smaller offset radius (80–120m) applied for visual separation.
|
||||||
|
|
||||||
|
6. **Assign confidence level**:
|
||||||
|
- **High confidence** (2+ anchors): Displayed at 55% opacity.
|
||||||
|
- **Low confidence** (1 anchor): Displayed at 30% opacity.
|
||||||
|
|
||||||
|
7. **Cache the result**: The computation is cached using a key derived from the contact's paths, anchor positions, path-history version, and radio parameters. The cache is only invalidated when any of these inputs change, avoiding recomputation on every UI rebuild.
|
||||||
|
|
||||||
|
#### How to read guessed locations on the map
|
||||||
|
|
||||||
|
- **Semi-transparent marker** with a `not_listed_location` icon: This is a guessed position, not a confirmed GPS fix.
|
||||||
|
- **More opaque** (55%): Higher confidence — the contact was seen through 2 or more repeaters with known positions.
|
||||||
|
- **More transparent** (30%): Lower confidence — based on a single repeater anchor only.
|
||||||
|
- Coordinates shown in the marker info dialog are prefixed with `~` to indicate they are estimated.
|
||||||
|
- Guessed locations can be toggled on/off in the map filter dialog (FAB → "Guessed locations" toggle).
|
||||||
|
|
||||||
|
## Map Interactions
|
||||||
|
|
||||||
|
### Zoom and Pan
|
||||||
|
Standard pinch-to-zoom (range 2–18). Initial camera position is calculated from the statistical spread of all plotted points.
|
||||||
|
|
||||||
|
### Tap on a Node Marker
|
||||||
|
Opens a dialog showing: type, path (hop chain), coordinates, last-seen time, and public key. Action buttons vary by type:
|
||||||
|
- **Chat nodes**: "Open Chat"
|
||||||
|
- **Repeaters**: "Manage Repeater"
|
||||||
|
- **Rooms**: "Join Room"
|
||||||
|
|
||||||
|
### Long-Press on Empty Map Area
|
||||||
|
Shows a bottom sheet with:
|
||||||
|
- **Share marker here**: Prompts for a label, then pick a DM contact or channel to send the location to. Wire format: `m:<lat>,<lon>|<label>|poi`
|
||||||
|
- **Set as my location**: Updates your device's advertised location
|
||||||
|
|
||||||
|
### Filter Dialog (FAB)
|
||||||
|
Toggle visibility of: chat nodes, repeaters, other nodes, guessed locations, discovery contacts.
|
||||||
|
Additional filters:
|
||||||
|
- **Key prefix filter**: Show only contacts whose public key starts with a given prefix
|
||||||
|
- **Last-seen time slider**: From 1 hour to "all time"
|
||||||
|
|
||||||
|
### Legend Card (Top-Right)
|
||||||
|
Shows node count and pin count. Tappable to expand a legend of all marker types.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Path Trace Map
|
||||||
|
|
||||||
|
### How to Access
|
||||||
|
- From the main map's radar icon
|
||||||
|
- From a contact's long-press menu → "Path Trace / Ping"
|
||||||
|
- From a message's path view → radar icon
|
||||||
|
|
||||||
|
### What the User Sees
|
||||||
|
A map with a polyline showing the route from your node through repeater hops to the target:
|
||||||
|
- **Green circles**: Hops with known GPS coordinates
|
||||||
|
- **Orange circles** (`~HH`): Inferred positions (no GPS but deducible from contacts)
|
||||||
|
- **Red endpoint**: Target contact with known GPS
|
||||||
|
- **Purple semi-transparent endpoint**: Target with guessed position
|
||||||
|
|
||||||
|
A legend card at the bottom lists each hop pair with SNR quality icons and total path distance.
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
Sends a trace request frame over the mesh. The repeater network traces the path hop-by-hop and returns per-hop SNR data. For hops without GPS, positions are inferred by averaging GPS coordinates of contacts sharing that last-hop byte.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Line-of-Sight (LOS) Analysis
|
||||||
|
|
||||||
|
### How to Access
|
||||||
|
From the main map, tap the terrain/antenna icon.
|
||||||
|
|
||||||
|
### What the User Sees
|
||||||
|
A full-screen map with a collapsible control panel containing:
|
||||||
|
- **Elevation profile chart**: Terrain fill (green), LOS beam line (white), radio horizon line (yellow)
|
||||||
|
- **Status**: Clear (green) or blocked (red) with distance and minimum clearance
|
||||||
|
- **Options panel**: Node toggles, endpoint dropdowns, antenna height sliders (0–400 ft), Run LOS button
|
||||||
|
|
||||||
|
### Key Interactions
|
||||||
|
- **Long-press the map** to add custom endpoints (orange pushpin markers, renameable/deleteable)
|
||||||
|
- **Tap a marker** to select it as Point A or B; LOS runs automatically when both are set
|
||||||
|
- **Antenna heights** are adjustable for both endpoints
|
||||||
|
- **Map line** between endpoints is colored green (clear) or red (blocked)
|
||||||
|
- Terrain elevation is fetched from the Open-Meteo API (21–81 sample points, cached 24 hours)
|
||||||
|
- K-factor is adjusted per radio frequency from a baseline of 4/3 at 915 MHz
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Offline Map Cache
|
||||||
|
|
||||||
|
### How to Access
|
||||||
|
Settings → App Settings → Map Display → Offline Map Cache
|
||||||
|
|
||||||
|
### What the User Sees
|
||||||
|
- Map with a blue polygon overlay showing previously selected cache bounds
|
||||||
|
- Bounding box coordinates card
|
||||||
|
- **Cache Area** controls: "Use Current View" and Clear buttons
|
||||||
|
- **Zoom Range** slider (3–18) with estimated tile count
|
||||||
|
- **Download progress** bar (when downloading)
|
||||||
|
- **Download Tiles** and **Clear Cache** buttons
|
||||||
|
|
||||||
|
### Key Interactions
|
||||||
|
1. Pan/zoom the map to the desired area
|
||||||
|
2. Tap "Use Current View" to capture the viewport as cache bounds
|
||||||
|
3. Adjust the zoom range slider
|
||||||
|
4. Tap "Download Tiles" (confirmation dialog shows estimated count)
|
||||||
|
5. Tiles are downloaded with up to 8 concurrent connections
|
||||||
|
6. Once cached, tiles are served from disk without internet (365-day stale period)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GPX Export
|
||||||
|
|
||||||
|
### How to Access
|
||||||
|
Settings → Export section
|
||||||
|
|
||||||
|
### What It Does
|
||||||
|
Exports contacts with GPS coordinates to a `.gpx` file via the OS share sheet. Three export options:
|
||||||
|
- **Export Repeaters**: Repeater and Room contacts with locations
|
||||||
|
- **Export Contacts**: Chat contacts with locations
|
||||||
|
- **Export All**: All contacts with locations
|
||||||
|
|
||||||
|
Each waypoint includes: name, lat/lon, type label, and public key hex.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Location Data Sources
|
||||||
|
|
||||||
|
The phone's own GPS is **never used**. All location data comes from the mesh:
|
||||||
|
|
||||||
|
1. **Device self-location**: Read from firmware device-info response. Set manually in Settings → Location, or updated automatically if the device has a GPS module.
|
||||||
|
2. **Remote node locations**: Extracted from advertisement packets received over the mesh. Encoded as integer lat/lon × 1,000,000.
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# Navigation
|
||||||
|
|
||||||
|
## App Flow
|
||||||
|
|
||||||
|
The app follows this general flow:
|
||||||
|
|
||||||
|
```
|
||||||
|
Launch → Scanner Screen → [Connect via BLE/USB/TCP] → Contacts Screen
|
||||||
|
```
|
||||||
|
|
||||||
|
After connecting, the three main screens (Contacts, Channels, Map) are accessible via a persistent bottom navigation bar called the **QuickSwitchBar**.
|
||||||
|
|
||||||
|
## Quick Switch Bar
|
||||||
|
|
||||||
|
The QuickSwitchBar is a Material 3 `NavigationBar` with a frosted-glass visual treatment (blur backdrop, transparent theme, rounded corners). It appears at the bottom of all three main screens.
|
||||||
|
|
||||||
|
| Index | Icon | Label | Screen |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 0 | People | Contacts | ContactsScreen |
|
||||||
|
| 1 | Tag | Channels | ChannelsScreen |
|
||||||
|
| 2 | Map | Map | MapScreen |
|
||||||
|
|
||||||
|
Tapping a tab replaces the current screen with a subtle fade + slight horizontal nudge transition (220ms forward, 200ms reverse). The back button is suppressed on all three main screens — navigation between them is flat, not stacked. All icons use outline variants (`people_outline`, `tag`, `map_outlined`) following Material 3 conventions.
|
||||||
|
|
||||||
|
## Device Screen
|
||||||
|
|
||||||
|
The Device Screen is a transitional hub that shows after connection. In practice, the app navigates directly to Contacts after connecting, but the Device Screen is reachable via the QuickSwitchBar.
|
||||||
|
|
||||||
|
### What the User Sees
|
||||||
|
|
||||||
|
**App Bar**:
|
||||||
|
- Left: Battery indicator chip (tappable — toggles between percentage and voltage display). Icon changes based on level: `battery_unknown` when data unavailable, `battery_alert` (orange) at 15% or below, `battery_full` otherwise
|
||||||
|
- Left-aligned title (`centerTitle: false`): Two-line layout — small grey "MeshCore" label above the device name in bold
|
||||||
|
- Right: Disconnect button (`bluetooth_disabled` crossed-out icon) and Settings button (tune icon)
|
||||||
|
|
||||||
|
**Body**:
|
||||||
|
- **Connection Card**: Device avatar, device name, device ID, "Connected" chip, and battery chip
|
||||||
|
- **Quick Switch** section: The QuickSwitchBar widget for navigating to Contacts/Channels/Map
|
||||||
|
|
||||||
|
### Disconnection
|
||||||
|
|
||||||
|
- The disconnect button shows a confirmation dialog before disconnecting
|
||||||
|
- If the device disconnects unexpectedly, the app automatically navigates back to the Scanner screen (fires after the current frame completes via a post-frame callback)
|
||||||
|
- This auto-navigation behavior (`DisconnectNavigationMixin`) is shared across all main screens
|
||||||
|
|
||||||
|
## Theme and Locale
|
||||||
|
|
||||||
|
- **Theme mode** is user-configurable in App Settings (System / Light / Dark) — not locked to system
|
||||||
|
- **Language** can be overridden to one of 15 supported languages, or follow the system locale
|
||||||
|
- On web, if a non-Chromium browser is detected, the app shows a `ChromeRequiredScreen` instead of the Scanner (Web Bluetooth requires Chromium)
|
||||||
|
|
||||||
|
## Full Navigation Graph
|
||||||
|
|
||||||
|
```
|
||||||
|
ScannerScreen (root, always on stack)
|
||||||
|
├─ [BLE connect] → push → ContactsScreen
|
||||||
|
├─ [TCP FAB] → push → TcpScreen
|
||||||
|
│ └─ [TCP connected] → pushReplacement → ContactsScreen
|
||||||
|
└─ [USB FAB] → push → UsbScreen
|
||||||
|
└─ [USB connected] → pushReplacement → ContactsScreen
|
||||||
|
|
||||||
|
ContactsScreen (selected=0)
|
||||||
|
├─ [quick-switch 1] → pushReplacement → ChannelsScreen
|
||||||
|
├─ [quick-switch 2] → pushReplacement → MapScreen
|
||||||
|
├─ [tap contact] → push → ChatScreen
|
||||||
|
├─ [overflow > Settings] → push → SettingsScreen
|
||||||
|
└─ [overflow > Discovered] → push → DiscoveryScreen
|
||||||
|
|
||||||
|
ChannelsScreen (selected=1)
|
||||||
|
├─ [quick-switch 0] → pushReplacement → ContactsScreen
|
||||||
|
├─ [quick-switch 2] → pushReplacement → MapScreen
|
||||||
|
├─ [tap channel] → push → ChannelChatScreen
|
||||||
|
└─ [overflow > Settings] → push → SettingsScreen
|
||||||
|
|
||||||
|
MapScreen (selected=2)
|
||||||
|
├─ [quick-switch 0] → pushReplacement → ContactsScreen
|
||||||
|
├─ [quick-switch 1] → pushReplacement → ChannelsScreen
|
||||||
|
├─ [radar button] → push → PathTraceMapScreen
|
||||||
|
├─ [terrain button] → push → LineOfSightMapScreen
|
||||||
|
└─ [long-press] → share marker / set location
|
||||||
|
|
||||||
|
Settings (push from any main screen)
|
||||||
|
└─ [App Settings] → push → AppSettingsScreen
|
||||||
|
└─ [Offline Map Cache] → push → MapCacheScreen
|
||||||
|
```
|
||||||
|
|
||||||
|
Any disconnection from any screen triggers `popUntil(route.isFirst)`, returning to the Scanner.
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# Notifications
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
MeshCore Open provides both **system notifications** (push-style OS alerts) and **in-app unread badges** to inform users of new activity.
|
||||||
|
|
||||||
|
## Notification Types
|
||||||
|
|
||||||
|
### 1. Direct Message Notifications
|
||||||
|
- **Triggered when**: A new incoming message arrives from a Chat or Room contact
|
||||||
|
- **Title**: Contact's name
|
||||||
|
- **Body**: Message text (reactions show "Reacted [emoji]", GIFs show "Sent a GIF")
|
||||||
|
- **Priority**: High
|
||||||
|
- **Android channel**: `messages`
|
||||||
|
|
||||||
|
### 2. Channel Message Notifications
|
||||||
|
- **Triggered when**: A new message arrives on a non-muted channel
|
||||||
|
- **Title**: Channel name (or "Channel N" if unnamed)
|
||||||
|
- **Body**: `"<senderName>: <message text>"`
|
||||||
|
- **Priority**: High
|
||||||
|
- **Android channel**: `channel_messages`
|
||||||
|
|
||||||
|
### 3. Advertisement Notifications
|
||||||
|
- **Triggered when**: A new node is discovered on the mesh for the first time
|
||||||
|
- **Title**: "New [type] discovered" (e.g., "New chat node discovered")
|
||||||
|
- **Body**: Contact's name
|
||||||
|
- **Priority**: Default
|
||||||
|
- **Android channel**: `adverts`
|
||||||
|
|
||||||
|
### 4. Background Service Notification (Android Only)
|
||||||
|
- A persistent low-priority notification: "MeshCore running — Keeping BLE connected"
|
||||||
|
- Required by Android for foreground services to keep BLE alive in the background
|
||||||
|
- Tap to re-launch the app
|
||||||
|
- **Does not auto-start on reboot** — the user must re-open the app manually after a phone restart
|
||||||
|
|
||||||
|
### Notification Tap Behavior
|
||||||
|
|
||||||
|
Tapping a notification currently re-launches the app at the root route. It does **not** navigate directly to the relevant chat or channel.
|
||||||
|
|
||||||
|
## In-App Unread Badges
|
||||||
|
|
||||||
|
Red numeric badges appear throughout the UI:
|
||||||
|
- **Contacts list**: Each contact row shows a red pill badge (e.g., "3") for unread messages
|
||||||
|
- **Channels list**: Each channel row shows an unread badge
|
||||||
|
- **Chat screen subtitle**: Shows unread count inline
|
||||||
|
- Badges cap at "99+" for display
|
||||||
|
|
||||||
|
### How Unread Counts Work
|
||||||
|
|
||||||
|
- Stored per contact (by public key) and per channel, **scoped to the connected device's identity** (first 10 hex characters of its public key). Switching between different radios gives each its own independent unread state
|
||||||
|
- **Suppressed when viewing**: Opening a chat resets the count to 0 and cancels the OS notification
|
||||||
|
- **Ignored for**: Outgoing messages, CLI messages, and repeater contacts
|
||||||
|
- Debounced writes (500ms) to avoid excessive storage I/O during message bursts
|
||||||
|
|
||||||
|
## Notification Settings
|
||||||
|
|
||||||
|
Access via **App Settings → Notifications**:
|
||||||
|
|
||||||
|
| Setting | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| Enable Notifications | On | Master toggle; requests OS permission when turned on |
|
||||||
|
| Message Notifications | On | DM alerts (greyed out if master is off) |
|
||||||
|
| Channel Message Notifications | On | Channel alerts (greyed out if master is off) |
|
||||||
|
| Advertisement Notifications | On | New node alerts (greyed out if master is off) |
|
||||||
|
|
||||||
|
### Per-Channel Muting
|
||||||
|
|
||||||
|
Long-press a channel in the channels list → "Mute channel" / "Unmute channel". Muted channels do not generate OS notifications.
|
||||||
|
|
||||||
|
There is no per-contact muting.
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
The notification system prevents notification storms:
|
||||||
|
- **Minimum interval**: 3 seconds between individual notifications
|
||||||
|
- **Batch window**: If multiple notifications arrive within 5 seconds, they are combined into a single summary notification on a fourth Android channel (`batch_summary`): "MeshCore Activity — 2 messages, 1 channel message, 3 new nodes". Note: batch summaries are Android-only; on Apple platforms individual notifications are shown
|
||||||
|
|
||||||
|
## Notification Clearing
|
||||||
|
|
||||||
|
- **Opening a contact chat**: Cancels the OS notification and resets unread count
|
||||||
|
- **Opening a channel**: Cancels the channel notification and resets unread count
|
||||||
|
- **Opening Contacts screen**: Cancels all advertisement notifications
|
||||||
|
|
||||||
|
## Platform Support
|
||||||
|
|
||||||
|
| Platform | Message Notifs | Badge | Background Service |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Android | Yes | Via notification number | Yes (foreground service) |
|
||||||
|
| iOS | Yes | Yes (app badge) | No |
|
||||||
|
| macOS | Yes | Yes | No |
|
||||||
|
| Windows | Yes | No | No |
|
||||||
|
| Linux | Yes (if D-Bus available) | No | No |
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
# Repeater Management
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Repeater Management provides tools for administering MeshCore repeater and room server nodes. It includes device status monitoring, CLI access, telemetry reading, neighbor discovery, and remote configuration.
|
||||||
|
|
||||||
|
## How to Access
|
||||||
|
|
||||||
|
From the Contacts screen:
|
||||||
|
1. Long-press a **Repeater** or **Room** contact
|
||||||
|
2. Select "Manage Repeater" or "Room Management"
|
||||||
|
3. Enter the admin password in the login dialog
|
||||||
|
4. Navigate to the Repeater Hub Screen
|
||||||
|
|
||||||
|
### Login Dialog
|
||||||
|
|
||||||
|
- Password field with show/hide toggle
|
||||||
|
- "Save password" checkbox (persists for future logins). If a saved password exists, it is pre-filled and the checkbox is pre-checked, making login one-tap
|
||||||
|
- Routing mode selector and "Manage Paths" link are available directly in the dialog (configure routing before login)
|
||||||
|
- Auto-retries up to 5 times on timeout, showing progress ("Attempt 2 of 5"). A wrong password stops immediately after the first attempt — only timeouts trigger retries
|
||||||
|
- After 5 failed attempts, further login attempts are blocked
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Repeater Hub Screen
|
||||||
|
|
||||||
|
The central management screen showing:
|
||||||
|
|
||||||
|
- **Header card**: Repeater name, short public key, path label, GPS coordinates (if known)
|
||||||
|
- **Battery chemistry selector**: NMC / LiFePO4 / LiPo (saved per repeater)
|
||||||
|
- **Management tool cards** (full-width cards with chevron arrows, not a grid). Title dynamically shows "Repeater Management" or "Room Management" based on contact type:
|
||||||
|
|
||||||
|
| Card | Destination |
|
||||||
|
|---|---|
|
||||||
|
| Status | Repeater Status Screen |
|
||||||
|
| Telemetry | Telemetry Screen |
|
||||||
|
| CLI | Repeater CLI Screen |
|
||||||
|
| Neighbors | Neighbors Screen |
|
||||||
|
| Settings | Repeater Settings Screen |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Repeater Status
|
||||||
|
|
||||||
|
### What the User Sees
|
||||||
|
|
||||||
|
Three information cards:
|
||||||
|
|
||||||
|
**System Information**:
|
||||||
|
- Battery percentage
|
||||||
|
- Uptime
|
||||||
|
- Queue length
|
||||||
|
- Error flags
|
||||||
|
- Clock at login time
|
||||||
|
|
||||||
|
**Radio Statistics**:
|
||||||
|
- Last RSSI and SNR
|
||||||
|
- Noise floor
|
||||||
|
- TX and RX airtime
|
||||||
|
|
||||||
|
**Packet Statistics**:
|
||||||
|
- Packets sent, received, and duplicates
|
||||||
|
- Broken down by flood vs. direct
|
||||||
|
|
||||||
|
### Key Interactions
|
||||||
|
- Auto-queries the repeater on open; shows a loading spinner until data arrives
|
||||||
|
- On timeout: red snackbar error. On success: data appears with a green snackbar confirmation
|
||||||
|
- Pull-to-refresh or refresh button to re-query
|
||||||
|
- Routing mode popup and path management dialog in app bar (these controls appear on **all** management sub-screens, not just Status)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Repeater CLI
|
||||||
|
|
||||||
|
A terminal-style interface for sending commands directly to the repeater.
|
||||||
|
|
||||||
|
### What the User Sees
|
||||||
|
|
||||||
|
- **Quick-command bar** (horizontal scroll): Shortcut buttons for common commands (get name, get radio, get tx, neighbors, ver, advert, clock)
|
||||||
|
- **Command history list**: Sent commands in primary color, responses in secondary color
|
||||||
|
- **Input bar**: Up/down history arrows, monospace text field with `> ` prefix, send button
|
||||||
|
|
||||||
|
### Key Interactions
|
||||||
|
|
||||||
|
- Type a command and press send (or Enter on desktop)
|
||||||
|
- Up/down arrows navigate through command history
|
||||||
|
- Quick-command buttons populate and send common commands
|
||||||
|
- Bug report icon: Shows raw frame debug info for the next typed command (shows error snackbar if input field is empty)
|
||||||
|
- Help icon: Opens a scrollable reference of all known CLI commands. Tapping any command populates the input field immediately
|
||||||
|
- Clear icon: Wipes the command/response history
|
||||||
|
- Failed/timed-out commands are automatically retried once
|
||||||
|
|
||||||
|
### Available CLI Commands
|
||||||
|
|
||||||
|
**General**: `advert`, `reboot`, `clock`, `password`, `ver`, `clear stats`
|
||||||
|
|
||||||
|
**Settings**: `set name`, `set af`, `set tx`, `set repeat`, `set allow.read.only`, `set flood.max`, `set int.thresh`, `set agc.reset.interval`, `set multi.acks`, `set advert.interval`, `set flood.advert.interval`, `set guest.password`, `set lat`, `set lon`, `set radio`, `set rxdelay`, `set txdelay`, `set direct.txdelay`, `set bridge.*`, `set adc.multiplier`, `tempradio`, `setperm`
|
||||||
|
|
||||||
|
**Bridge**: `get bridge.type`
|
||||||
|
|
||||||
|
**Logging**: `log start`, `log stop`, `log erase`
|
||||||
|
|
||||||
|
**Neighbors**: `neighbors`, `neighbor.remove`
|
||||||
|
|
||||||
|
**Region Management**: `region`, `region load/get/put/remove/allowf/denyf/home/save`
|
||||||
|
|
||||||
|
**GPS**: `gps`, `gps on/off/sync/setloc/advert`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Telemetry
|
||||||
|
|
||||||
|
### What the User Sees
|
||||||
|
|
||||||
|
A list of Cayenne LPP sensor channel cards:
|
||||||
|
|
||||||
|
- **Channel 1** (special): Battery voltage (shown as percentage or raw mV) and MCU temperature
|
||||||
|
- **Other channels**: Raw sensor values with appropriate labels
|
||||||
|
|
||||||
|
Shows "No data" until a response arrives from the repeater.
|
||||||
|
|
||||||
|
### Key Interactions
|
||||||
|
- Auto-queries on open
|
||||||
|
- Pull-to-refresh
|
||||||
|
- Temperature respects metric/imperial setting
|
||||||
|
- Battery readings are stored for the repeater's battery snapshot
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Neighbors
|
||||||
|
|
||||||
|
### What the User Sees
|
||||||
|
|
||||||
|
A card titled "Repeater's Neighbors - N" listing each neighbor as:
|
||||||
|
- Repeater name (or hex key prefix if unknown)
|
||||||
|
- Time since last heard
|
||||||
|
- SNR quality icon with color coding and label
|
||||||
|
|
||||||
|
### Key Interactions
|
||||||
|
- Auto-queries up to 15 neighbors on open
|
||||||
|
- Matches public key prefixes against known contacts to show names
|
||||||
|
- Pull-to-refresh
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Repeater Settings
|
||||||
|
|
||||||
|
### What the User Sees
|
||||||
|
|
||||||
|
Five configuration cards:
|
||||||
|
|
||||||
|
**1. Basic Settings**
|
||||||
|
- Name field
|
||||||
|
- Admin password field
|
||||||
|
- Guest password field
|
||||||
|
|
||||||
|
**2. Radio Settings**
|
||||||
|
- Frequency (MHz)
|
||||||
|
- TX Power (dBm)
|
||||||
|
- Bandwidth dropdown (kHz)
|
||||||
|
- Spreading Factor (SF5–SF12)
|
||||||
|
- Coding Rate (4/5–4/8)
|
||||||
|
|
||||||
|
**3. Location Settings**
|
||||||
|
- Latitude and longitude fields
|
||||||
|
|
||||||
|
**4. Features**
|
||||||
|
- Packet forwarding toggle
|
||||||
|
- Guest access toggle
|
||||||
|
|
||||||
|
**5. Advertisement Settings**
|
||||||
|
- Local advert interval slider (60–240 minutes) with enable/disable toggle
|
||||||
|
- Flood advert interval slider (3–168 hours) with enable/disable toggle
|
||||||
|
|
||||||
|
**6. Danger Zone** (red-styled card)
|
||||||
|
- Reboot repeater
|
||||||
|
- Erase filesystem (serial-only warning)
|
||||||
|
|
||||||
|
### Key Interactions
|
||||||
|
- **Settings are NOT auto-fetched on open**. Only name and location are pre-filled from locally cached contact data. You must tap each section's refresh button to fetch live values from the repeater
|
||||||
|
- TX Power has its own separate refresh button, independent from the main Radio Settings refresh
|
||||||
|
- Save button appears when changes are detected
|
||||||
|
- Settings are sent sequentially with 200ms delays between commands (fire-and-forget, no per-command acknowledgment wait)
|
||||||
|
- Validation prevents invalid values (e.g., frequency range, LoRa parameter compatibility)
|
||||||
|
- Advertisement interval sliders reset to defaults when re-enabled (local: 60 min, flood: 3 hours)
|
||||||
|
- **Erase Filesystem** does NOT send any command over the air — tapping it only shows a snackbar explaining the operation requires physical serial access. It is effectively non-functional when connected wirelessly
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
# Scanner & Connection
|
||||||
|
|
||||||
|
## BLE Scanner (Home Screen)
|
||||||
|
|
||||||
|
The BLE Scanner is the app's home screen, displayed immediately on launch.
|
||||||
|
|
||||||
|
### How to Access
|
||||||
|
|
||||||
|
- Opens automatically when the app starts
|
||||||
|
- Returns here when disconnecting from any device
|
||||||
|
- Accessible by navigating back from a connected session
|
||||||
|
|
||||||
|
### What the User Sees
|
||||||
|
|
||||||
|
**App Bar**: Centered title "Scanner".
|
||||||
|
|
||||||
|
**Bluetooth-Off Warning Banner** (conditional): Appears when the Bluetooth adapter is off, showing a `bluetooth_disabled` icon, a warning message, and on Android, an "Enable Bluetooth" button.
|
||||||
|
|
||||||
|
**Status Bar**: A full-width colored strip reflecting the current connection state:
|
||||||
|
|
||||||
|
| State | Text | Color |
|
||||||
|
|---|---|---|
|
||||||
|
| Disconnected | "Not connected" | Grey |
|
||||||
|
| Scanning | "Scanning..." | Blue |
|
||||||
|
| Connecting | "Connecting..." | Orange |
|
||||||
|
| Connected | "Connected to \<device name\>" | Green |
|
||||||
|
| Disconnecting | "Disconnecting..." | Orange |
|
||||||
|
|
||||||
|
**Device List**: When no devices are found, shows a large Bluetooth icon with a prompt. The prompt text is dynamic: "Searching for devices..." while actively scanning, or "Tap Scan to search" when idle. When devices are found, shows a scrollable list of `DeviceTile` widgets.
|
||||||
|
|
||||||
|
**Bottom FAB Row**: Up to three floating action buttons:
|
||||||
|
- **USB** button - Opens USB connection screen (Android, Windows, Linux, macOS, Chrome web only)
|
||||||
|
- **TCP/IP** button - Opens TCP connection screen (all non-web platforms)
|
||||||
|
- **BLE Scan** button - Toggles BLE scanning on/off; shows a spinner when scanning. **Disabled** (greyed out, not tappable) when Bluetooth is off
|
||||||
|
|
||||||
|
### Device Tile
|
||||||
|
|
||||||
|
Each discovered device is displayed as a list tile showing:
|
||||||
|
- **Signal strength icon** (color-coded by RSSI):
|
||||||
|
- Green: >= -60 dBm (excellent)
|
||||||
|
- Light green: -60 to -70 dBm (good)
|
||||||
|
- Amber: -70 to -80 dBm (fair)
|
||||||
|
- Orange: -80 to -90 dBm (weak)
|
||||||
|
- Red: < -90 dBm (poor)
|
||||||
|
- **RSSI value** in dBm (e.g., "-72 dBm")
|
||||||
|
- **Device name** (falls back to "Unknown Device")
|
||||||
|
- **Device ID** (BLE MAC address on Android; a system-assigned UUID on iOS/macOS)
|
||||||
|
- **Connect button** (the entire tile row is also tappable — both trigger connection)
|
||||||
|
|
||||||
|
Note: The weak (-80 to -90 dBm) and poor (< -90 dBm) tiers share the same icon shape and are only differentiated by color (orange vs. red).
|
||||||
|
|
||||||
|
### How Scanning Works
|
||||||
|
|
||||||
|
- Filters for devices with names starting with `MeshCore-` or `Whisper-`
|
||||||
|
- Uses low-latency scan mode on Android
|
||||||
|
- Scans for 10 seconds then auto-stops
|
||||||
|
- On iOS/macOS, waits for BLE adapter initialization before starting
|
||||||
|
- If Bluetooth is turned off during a scan, scanning stops immediately
|
||||||
|
|
||||||
|
### Connecting to a Device
|
||||||
|
|
||||||
|
Tap a device tile or its Connect button:
|
||||||
|
1. The connector stops scanning and transitions to "connecting"
|
||||||
|
2. Connects to the device with a 15-second timeout
|
||||||
|
3. Requests MTU 185 bytes for optimal throughput
|
||||||
|
4. Discovers BLE services and locates the Nordic UART Service
|
||||||
|
5. Subscribes to TX notifications for receiving data
|
||||||
|
6. On success, automatically navigates to the Contacts screen
|
||||||
|
7. On failure, shows a red error snackbar
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## USB Connection
|
||||||
|
|
||||||
|
### How to Access
|
||||||
|
|
||||||
|
From the Scanner screen, tap the **USB** FAB button.
|
||||||
|
|
||||||
|
### What the User Sees
|
||||||
|
|
||||||
|
- A colored status bar at the top (same color scheme as BLE scanner)
|
||||||
|
- A list of detected USB serial ports, each showing:
|
||||||
|
- Friendly display name
|
||||||
|
- Raw port name (subtitle, only shown when it differs from the display name)
|
||||||
|
- "Connect" button
|
||||||
|
- FABs at the bottom to switch to BLE or TCP (these use `pushReplacement`, so back navigation returns to Scanner, not between USB/TCP)
|
||||||
|
|
||||||
|
### Key Interactions
|
||||||
|
|
||||||
|
- On desktop (Windows, Linux, macOS): ports are polled every 2 seconds for hot-plug detection (polling pauses while connecting/connected)
|
||||||
|
- On mobile: tap the "Scan" FAB to manually refresh
|
||||||
|
- Tap a port or its Connect button to connect
|
||||||
|
- On successful connection, navigates to Contacts screen
|
||||||
|
- On connection failure, the port list automatically refreshes
|
||||||
|
- Platform-specific error messages for common USB failures (permission denied, device missing, device detached, device busy, driver missing, port invalid, timeout, and more)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TCP Connection
|
||||||
|
|
||||||
|
### How to Access
|
||||||
|
|
||||||
|
From the Scanner screen, tap the **TCP/IP** FAB button.
|
||||||
|
|
||||||
|
### What the User Sees
|
||||||
|
|
||||||
|
- A colored status bar at the top
|
||||||
|
- **Host address** text field
|
||||||
|
- **Port number** text field
|
||||||
|
- **Connect** button
|
||||||
|
- FABs at the bottom to switch to USB or BLE
|
||||||
|
|
||||||
|
### Key Interactions
|
||||||
|
|
||||||
|
- Last-used host and port are pre-populated from saved settings
|
||||||
|
- Tap Connect to validate inputs and connect
|
||||||
|
- Host must not be empty
|
||||||
|
- Port must be a number between 1 and 65535
|
||||||
|
- Validation errors are shown as red snackbars
|
||||||
|
- The Connect button shows a spinner and "Connecting..." label while in progress
|
||||||
|
- The status bar shows the specific host:port being connected to (e.g., "Connecting to 192.168.1.1:5000")
|
||||||
|
- On success, navigates to Contacts screen and saves the host/port to settings
|
||||||
|
- On connection, the status bar shows the active TCP endpoint (e.g., "Connected to 192.168.1.1:5000")
|
||||||
|
- Error messages for timeout, unsupported platform, and connection failures
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
# Settings
|
||||||
|
|
||||||
|
## How to Access
|
||||||
|
|
||||||
|
- From the Device Screen: tap the tune/sliders icon in the app bar
|
||||||
|
- From Contacts or Channels: overflow menu (three-dot) → Settings
|
||||||
|
|
||||||
|
Settings are only accessible while a device is connected.
|
||||||
|
|
||||||
|
## Settings Screen Layout
|
||||||
|
|
||||||
|
The settings screen is a scrollable list of cards:
|
||||||
|
|
||||||
|
1. [Device Info](#device-info)
|
||||||
|
2. [App Settings](#app-settings) (link to sub-screen)
|
||||||
|
3. [Node Settings](#node-settings)
|
||||||
|
4. [Actions](#actions)
|
||||||
|
5. [Debug](#debug)
|
||||||
|
6. [Export](#export)
|
||||||
|
7. [About](#about)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Device Info
|
||||||
|
|
||||||
|
A collapsible card showing read-only device information. **Collapsed by default** — tap the header to expand with an animated chevron indicator:
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|---|---|
|
||||||
|
| Name | Connected device's display name |
|
||||||
|
| ID | Device identifier |
|
||||||
|
| Status | Connected / Disconnected |
|
||||||
|
| Battery | Percentage or voltage (tap to toggle) |
|
||||||
|
| Node Name | The node's mesh identity name |
|
||||||
|
| Public Key | First 16 hex characters + "..." |
|
||||||
|
| Contacts Count | Number of known contacts |
|
||||||
|
| Channel Count | Number of configured channels |
|
||||||
|
|
||||||
|
Battery shows an alert icon and orange text when at 15% or below. The toggle only works when millivolt data is available from the firmware.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## App Settings
|
||||||
|
|
||||||
|
A dedicated sub-screen for app-level preferences (nothing here is sent to the device). All settings persist locally via SharedPreferences.
|
||||||
|
|
||||||
|
### Appearance
|
||||||
|
- **Theme**: System / Light / Dark
|
||||||
|
- **Language**: System default or one of 15 languages (English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian)
|
||||||
|
- **Enable Message Tracing**: Shows path trace overlays and extra metadata on messages
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
- **Master enable/disable**: Requests OS permission when enabling
|
||||||
|
- **Message notifications**: New direct message alerts
|
||||||
|
- **Channel message notifications**: New channel message alerts
|
||||||
|
- **Advertisement notifications**: New node discovery alerts
|
||||||
|
|
||||||
|
### Messaging
|
||||||
|
- **Clear Path on Max Retry**: Erases the stored routing path after all retries fail
|
||||||
|
- **Auto Route Rotation**: Enables weighted routing algorithm. When enabled, expands to show five slider sub-settings (hidden when off):
|
||||||
|
- Max Route Weight (1–10, default 5, integer steps)
|
||||||
|
- Initial Route Weight (0.5–5.0, default 3.0)
|
||||||
|
- Success Increment (0.1–2.0, default 0.5, 0.1 steps)
|
||||||
|
- Failure Decrement (0.1–2.0, default 0.2, 0.1 steps)
|
||||||
|
- Max Message Retries (2–10, default 5)
|
||||||
|
|
||||||
|
### Battery
|
||||||
|
- **Battery Chemistry**: NMC / LiFePO4 / LiPo (per device, used to calibrate percentage from voltage)
|
||||||
|
|
||||||
|
### Map Display
|
||||||
|
- **Show Repeaters**: Toggle repeater markers on map
|
||||||
|
- **Show Chat Nodes**: Toggle chat node markers
|
||||||
|
- **Show Other Nodes**: Toggle room/sensor markers
|
||||||
|
- **Time Filter**: All time / Last 1h / Last 6h / Last 24h / Last week
|
||||||
|
- **Units**: Metric / Imperial
|
||||||
|
- **Offline Map Cache**: Navigate to tile download screen
|
||||||
|
|
||||||
|
### Debug
|
||||||
|
- **App Debug Logging**: Enable the in-app debug log
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Node Settings
|
||||||
|
|
||||||
|
These settings are sent directly to the connected device firmware.
|
||||||
|
|
||||||
|
### Node Name
|
||||||
|
- Opens a dialog with a text field (max 31 characters)
|
||||||
|
- Sends the new name to the device
|
||||||
|
- Confirmed via snackbar
|
||||||
|
|
||||||
|
### Radio Settings
|
||||||
|
Opens a dialog pre-populated with the device's current radio settings. Contains:
|
||||||
|
- **Preset dropdown**: 19 regional presets — selecting a preset immediately fills all fields below. Full list: Australia, Australia (Narrow), Australia SA/WA/QLD, Czech Republic, EU 433MHz, EU/UK (Long Range), EU/UK (Medium Range), EU/UK (Narrow), New Zealand, New Zealand (Narrow), Portugal 433, Portugal 869, Switzerland, USA Arizona, USA/Canada, Vietnam, Off-Grid 433, Off-Grid 869, Off-Grid 918
|
||||||
|
- **Frequency** (MHz): Free text, validated 300–2500 MHz
|
||||||
|
- **Bandwidth**: Dropdown (7.8 / 10.4 / 15.6 / 20.8 / 31.25 / 41.7 / 62.5 / 125 / 250 / 500 kHz)
|
||||||
|
- **Spreading Factor**: SF5–SF12
|
||||||
|
- **Coding Rate**: 4/5, 4/6, 4/7, 4/8
|
||||||
|
- **TX Power** (dBm): Validated 0 to device max (typically 22 dBm)
|
||||||
|
- **Client Repeat** toggle: Only shown on firmware v9+; requires frequency to be exactly 433.000, 869.000, or 918.000 MHz (the Off-Grid presets). Save is blocked with a warning if enabled on other frequencies
|
||||||
|
|
||||||
|
### Location
|
||||||
|
Opens a dialog pre-populated with the device's current coordinates (if known):
|
||||||
|
- Latitude and longitude fields (decimal, 6 decimal places). If only one field is provided, the other uses the device's current value
|
||||||
|
- If GPS-capable hardware (detected via `gps` custom variable):
|
||||||
|
- GPS Update Interval (seconds, 60–86399, default 900 = 15 minutes). Validated and sent separately before lat/lon
|
||||||
|
- Enable GPS toggle (takes effect immediately, not deferred to Save)
|
||||||
|
- Validation: lat ±90, lon ±180
|
||||||
|
|
||||||
|
### Contact Settings
|
||||||
|
Five toggles controlling which node types are auto-added when heard:
|
||||||
|
- Auto-add Chat Users
|
||||||
|
- Auto-add Repeaters
|
||||||
|
- Auto-add Room Servers
|
||||||
|
- Auto-add Sensors
|
||||||
|
- Overwrite Oldest (when contact list is full)
|
||||||
|
|
||||||
|
### Privacy Mode
|
||||||
|
Opens a confirmation dialog with three buttons: Cancel, Enable, and Disable. Both states can be set from the same dialog regardless of current state. A snackbar confirms which state was applied. When on, the node stops broadcasting its location in advertisements.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
One-tap device operations:
|
||||||
|
|
||||||
|
| Action | Description |
|
||||||
|
|---|---|
|
||||||
|
| Send Advertisement | Floods the mesh with your node's advertisement |
|
||||||
|
| Sync Time | Sends current Unix timestamp to the device |
|
||||||
|
| Refresh Contacts | Re-requests the full contact list |
|
||||||
|
| Reboot Device | Confirmation dialog → reboots the device (shown in orange) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debug
|
||||||
|
|
||||||
|
Two log viewers accessible via list tiles:
|
||||||
|
|
||||||
|
### BLE Debug Log
|
||||||
|
Two views (togglable via segmented button):
|
||||||
|
- **Frames view**: Direction icon, description, hex preview, timestamp per frame. Long-press to copy hex.
|
||||||
|
- **Raw Log RX view**: Decoded LoRa packets with route type, payload type, path, and summary.
|
||||||
|
- Copy-all and Clear buttons in the app bar.
|
||||||
|
|
||||||
|
### App Debug Log
|
||||||
|
Structured log entries (Info / Warning / Error), with tag, message, and timestamp.
|
||||||
|
- Must be enabled first in App Settings → Debug
|
||||||
|
- Copy-all and Clear buttons
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Export
|
||||||
|
|
||||||
|
Three GPX export options (not available on web):
|
||||||
|
|
||||||
|
| Option | Exports |
|
||||||
|
|---|---|
|
||||||
|
| Export Repeaters | Repeaters and Rooms with GPS coordinates |
|
||||||
|
| Export Contacts | Chat contacts with GPS coordinates |
|
||||||
|
| Export All | All contacts with GPS coordinates |
|
||||||
|
|
||||||
|
Each creates a `.gpx` file and opens the OS share sheet. Feedback via snackbar for four outcomes: success, no contacts with coordinates, feature not available (web), or error.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## About
|
||||||
|
|
||||||
|
Shows the standard Flutter about dialog with app name, version, and legal notice.
|
||||||
+1812
-415
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,70 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import '../services/app_debug_log_service.dart';
|
||||||
|
import '../services/tcp_transport_service.dart';
|
||||||
|
|
||||||
|
/// Manages TCP transport for MeshCore devices.
|
||||||
|
///
|
||||||
|
/// Owns the [TcpTransportService] and TCP-specific connection state.
|
||||||
|
/// The main [MeshCoreConnector] delegates all TCP operations here.
|
||||||
|
class MeshCoreTcpConnector {
|
||||||
|
final TcpTransportService _service = TcpTransportService();
|
||||||
|
AppDebugLogService? _debugLog;
|
||||||
|
StreamSubscription<Uint8List>? _frameSubscription;
|
||||||
|
|
||||||
|
// --- Getters ---
|
||||||
|
String? get activeEndpoint => _service.activeEndpoint;
|
||||||
|
bool get isConnected => _service.isConnected;
|
||||||
|
|
||||||
|
// --- Configuration ---
|
||||||
|
void setDebugLogService(AppDebugLogService? service) {
|
||||||
|
_debugLog = service;
|
||||||
|
_service.setDebugLogService(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Connection lifecycle ---
|
||||||
|
Future<void> connect({required String host, required int port}) async {
|
||||||
|
_debugLog?.info('TcpConnector.connect endpoint=$host:$port', tag: 'TCP');
|
||||||
|
await _frameSubscription?.cancel();
|
||||||
|
_frameSubscription = null;
|
||||||
|
await _service.connect(host: host, port: port);
|
||||||
|
_debugLog?.info(
|
||||||
|
'TcpConnector.connect done, endpoint=${_service.activeEndpoint}',
|
||||||
|
tag: 'TCP',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamSubscription<Uint8List> listenFrames({
|
||||||
|
required void Function(Uint8List) onFrame,
|
||||||
|
required void Function(Object, StackTrace?) onError,
|
||||||
|
required void Function() onDone,
|
||||||
|
}) {
|
||||||
|
_frameSubscription = _service.frameStream.listen(
|
||||||
|
onFrame,
|
||||||
|
onError: onError,
|
||||||
|
onDone: onDone,
|
||||||
|
);
|
||||||
|
return _frameSubscription!;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> cancelFrameSubscription() async {
|
||||||
|
await _frameSubscription?.cancel();
|
||||||
|
_frameSubscription = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> disconnect() async {
|
||||||
|
if (!_service.isConnected && _frameSubscription == null) return;
|
||||||
|
_debugLog?.info('TcpConnector.disconnect', tag: 'TCP');
|
||||||
|
await _frameSubscription?.cancel();
|
||||||
|
_frameSubscription = null;
|
||||||
|
await _service.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> write(Uint8List data) => _service.write(data);
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_frameSubscription?.cancel();
|
||||||
|
_service.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import '../services/app_debug_log_service.dart';
|
||||||
|
import '../services/usb_serial_service.dart';
|
||||||
|
|
||||||
|
/// Manages USB serial transport for MeshCore devices.
|
||||||
|
///
|
||||||
|
/// Owns the [UsbSerialService] and USB-specific connection state.
|
||||||
|
/// The main [MeshCoreConnector] delegates all USB operations here.
|
||||||
|
class MeshCoreUsbManager {
|
||||||
|
MeshCoreUsbManager();
|
||||||
|
|
||||||
|
final UsbSerialService _service = UsbSerialService();
|
||||||
|
AppDebugLogService? _debugLog;
|
||||||
|
String? _activePortKey;
|
||||||
|
String? _activePortLabel;
|
||||||
|
|
||||||
|
// --- Getters ---
|
||||||
|
String? get activePortKey => _activePortKey;
|
||||||
|
String? get activePortDisplayLabel => _activePortLabel ?? _activePortKey;
|
||||||
|
bool get isConnected => _service.isConnected;
|
||||||
|
Stream<Uint8List> get frameStream => _service.frameStream;
|
||||||
|
|
||||||
|
// --- Configuration ---
|
||||||
|
Future<List<String>> listPorts() => _service.listPorts();
|
||||||
|
|
||||||
|
void setRequestPortLabel(String label) => _service.setRequestPortLabel(label);
|
||||||
|
|
||||||
|
void setFallbackDeviceName(String label) =>
|
||||||
|
_service.setFallbackDeviceName(label);
|
||||||
|
|
||||||
|
void setDebugLogService(AppDebugLogService? service) {
|
||||||
|
_debugLog = service;
|
||||||
|
_service.setDebugLogService(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Connection lifecycle ---
|
||||||
|
Future<void> connect({
|
||||||
|
required String portName,
|
||||||
|
int baudRate = 115200,
|
||||||
|
}) async {
|
||||||
|
_debugLog?.info(
|
||||||
|
'UsbManager.connect: portName=$portName baud=$baudRate',
|
||||||
|
tag: 'USB',
|
||||||
|
);
|
||||||
|
await _service.connect(portName: portName, baudRate: baudRate);
|
||||||
|
_activePortKey = _service.activePortKey ?? portName;
|
||||||
|
_activePortLabel = _service.activePortDisplayLabel ?? portName;
|
||||||
|
_debugLog?.info(
|
||||||
|
'UsbManager.connect: done, key=$_activePortKey label=$_activePortLabel',
|
||||||
|
tag: 'USB',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> disconnect() async {
|
||||||
|
if (!_service.isConnected && _activePortKey == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_debugLog?.info('UsbManager.disconnect', tag: 'USB');
|
||||||
|
await _service.disconnect();
|
||||||
|
_activePortKey = null;
|
||||||
|
_activePortLabel = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> write(Uint8List data) => _service.write(data);
|
||||||
|
|
||||||
|
Future<void> writeRaw(Uint8List data) => _service.writeRaw(data);
|
||||||
|
|
||||||
|
// --- Label management ---
|
||||||
|
void updateConnectedLabel(String selfName) {
|
||||||
|
_service.updateConnectedLabel(selfName);
|
||||||
|
_activePortLabel = _service.activePortDisplayLabel ?? _activePortLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_service.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
// Buffer Reader - sequential binary data reader with pointer tracking
|
// Buffer Reader - sequential binary data reader with pointer tracking
|
||||||
class BufferReader {
|
class BufferReader {
|
||||||
int _pointer = 0;
|
int _pointer = 0;
|
||||||
|
int _lastPointer = 0;
|
||||||
final Uint8List _buffer;
|
final Uint8List _buffer;
|
||||||
|
|
||||||
BufferReader(Uint8List data) : _buffer = Uint8List.fromList(data);
|
BufferReader(Uint8List data) : _buffer = Uint8List.fromList(data);
|
||||||
@@ -13,6 +16,7 @@ class BufferReader {
|
|||||||
int readByte() => readBytes(1)[0];
|
int readByte() => readBytes(1)[0];
|
||||||
|
|
||||||
Uint8List readBytes(int count) {
|
Uint8List readBytes(int count) {
|
||||||
|
_lastPointer = _pointer;
|
||||||
if (_pointer + count > _buffer.length) {
|
if (_pointer + count > _buffer.length) {
|
||||||
throw RangeError(
|
throw RangeError(
|
||||||
'Attempted to read $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
|
'Attempted to read $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
|
||||||
@@ -24,6 +28,7 @@ class BufferReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void skipBytes(int count) {
|
void skipBytes(int count) {
|
||||||
|
_lastPointer = _pointer;
|
||||||
if (_pointer + count > _buffer.length) {
|
if (_pointer + count > _buffer.length) {
|
||||||
throw RangeError(
|
throw RangeError(
|
||||||
'Attempted to skip $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
|
'Attempted to skip $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
|
||||||
@@ -34,10 +39,8 @@ class BufferReader {
|
|||||||
|
|
||||||
Uint8List readRemainingBytes() => readBytes(remaining);
|
Uint8List readRemainingBytes() => readBytes(remaining);
|
||||||
|
|
||||||
String readString() =>
|
String readCStringGreedy(int maxLength) {
|
||||||
utf8.decode(readRemainingBytes(), allowMalformed: true);
|
_lastPointer = _pointer;
|
||||||
|
|
||||||
String readCString(int maxLength) {
|
|
||||||
final value = <int>[];
|
final value = <int>[];
|
||||||
final bytes = readBytes(maxLength);
|
final bytes = readBytes(maxLength);
|
||||||
for (final byte in bytes) {
|
for (final byte in bytes) {
|
||||||
@@ -51,6 +54,25 @@ class BufferReader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String readCString({int maxLength = -1}) {
|
||||||
|
final backupPointer = _pointer;
|
||||||
|
final value = <int>[];
|
||||||
|
int counter = 0;
|
||||||
|
final maxLen = maxLength >= 0 ? maxLength : remaining;
|
||||||
|
while (counter < maxLen) {
|
||||||
|
final byte = readByte();
|
||||||
|
if (byte == 0) break;
|
||||||
|
value.add(byte);
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
_lastPointer = backupPointer;
|
||||||
|
try {
|
||||||
|
return utf8.decode(Uint8List.fromList(value), allowMalformed: true);
|
||||||
|
} catch (e) {
|
||||||
|
return String.fromCharCodes(value); // Latin-1 fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int readUInt8() => readBytes(1).buffer.asByteData().getUint8(0);
|
int readUInt8() => readBytes(1).buffer.asByteData().getUint8(0);
|
||||||
int readInt8() => readBytes(1).buffer.asByteData().getInt8(0);
|
int readInt8() => readBytes(1).buffer.asByteData().getInt8(0);
|
||||||
int readUInt16LE() =>
|
int readUInt16LE() =>
|
||||||
@@ -72,6 +94,9 @@ class BufferReader {
|
|||||||
if ((value & 0x800000) != 0) value -= 0x1000000;
|
if ((value & 0x800000) != 0) value -= 0x1000000;
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void resetPointer() => _pointer = 0;
|
||||||
|
void rewind() => _pointer = _lastPointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buffer Writer - accumulating binary data builder
|
// Buffer Writer - accumulating binary data builder
|
||||||
@@ -114,6 +139,24 @@ class BufferWriter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void writeHex(String hex) {
|
void writeHex(String hex) {
|
||||||
|
writeBytes(hex2Uint8List(hex));
|
||||||
|
}
|
||||||
|
|
||||||
|
void writeBytesPadded(Uint8List bytes, int totalLength) {
|
||||||
|
// Path data (64 bytes, zero-padded)
|
||||||
|
final bytesPadded = Uint8List(totalLength);
|
||||||
|
final len = bytes.length < totalLength ? bytes.length : totalLength;
|
||||||
|
if (bytes.isNotEmpty && len > 0) {
|
||||||
|
final copyLen = bytes.length < totalLength ? bytes.length : totalLength;
|
||||||
|
for (int i = 0; i < copyLen; i++) {
|
||||||
|
bytesPadded[i] = bytes[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeBytes(bytesPadded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint8List hex2Uint8List(String hex) {
|
||||||
// Validate hex string length is even and not empty
|
// Validate hex string length is even and not empty
|
||||||
if (hex.isEmpty || hex.length % 2 != 0) {
|
if (hex.isEmpty || hex.length % 2 != 0) {
|
||||||
throw FormatException('Invalid hex string length: ${hex.length}');
|
throw FormatException('Invalid hex string length: ${hex.length}');
|
||||||
@@ -123,14 +166,11 @@ class BufferWriter {
|
|||||||
final hexByte = hex.substring(i * 2, i * 2 + 2);
|
final hexByte = hex.substring(i * 2, i * 2 + 2);
|
||||||
final byte = int.tryParse(hexByte, radix: 16);
|
final byte = int.tryParse(hexByte, radix: 16);
|
||||||
if (byte == null) {
|
if (byte == null) {
|
||||||
throw FormatException(
|
throw FormatException('Invalid hex characters at position $i: $hexByte');
|
||||||
'Invalid hex characters at position $i: $hexByte',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
result.add(byte);
|
result.add(byte);
|
||||||
}
|
}
|
||||||
writeBytes(Uint8List.fromList(result));
|
return Uint8List.fromList(result);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command codes (to device)
|
// Command codes (to device)
|
||||||
@@ -162,15 +202,18 @@ const int cmdGetChannel = 31;
|
|||||||
const int cmdSetChannel = 32;
|
const int cmdSetChannel = 32;
|
||||||
const int cmdSendTracePath = 36;
|
const int cmdSendTracePath = 36;
|
||||||
const int cmdSetOtherParams = 38;
|
const int cmdSetOtherParams = 38;
|
||||||
const int cmdGetRadioSettings = 57;
|
const int cmdSendAnonReq = 57;
|
||||||
const int cmdGetTelemetryReq = 39;
|
const int cmdSendTelemetryReq = 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;
|
||||||
const int txtTypeCliData = 1;
|
const int txtTypeCliData = 1;
|
||||||
|
const int txtTypeSigned = 2;
|
||||||
|
|
||||||
// Repeater request types (for server requests)
|
// Repeater request types (for server requests)
|
||||||
const int reqTypeGetStatus = 0x01;
|
const int reqTypeGetStatus = 0x01;
|
||||||
@@ -200,8 +243,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;
|
||||||
@@ -223,6 +266,10 @@ const int advTypeRepeater = 2;
|
|||||||
const int advTypeRoom = 3;
|
const int advTypeRoom = 3;
|
||||||
const int advTypeSensor = 4;
|
const int advTypeSensor = 4;
|
||||||
|
|
||||||
|
const int teleModeDeny = 0;
|
||||||
|
const int teleModeAllowFlags = 1; // use contact.flags
|
||||||
|
const int teleModeAllowAll = 2;
|
||||||
|
|
||||||
// Payload Types
|
// Payload Types
|
||||||
const int payloadTypeREQ =
|
const int payloadTypeREQ =
|
||||||
0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
|
0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
|
||||||
@@ -247,8 +294,21 @@ const int payloadTypeCONTROL = 0x0B; // a control/discovery packet
|
|||||||
const int payloadTypeRawCustom =
|
const int payloadTypeRawCustom =
|
||||||
0x0F; // custom packet as raw bytes, for applications with custom encryption, payloads, etc
|
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 signatureSize = 64;
|
||||||
const int maxPathSize = 64;
|
const int maxPathSize = 64;
|
||||||
const int pathHashSize = 1;
|
const int pathHashSize = 1;
|
||||||
const int maxNameSize = 32;
|
const int maxNameSize = 32;
|
||||||
@@ -291,13 +351,16 @@ 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 contactFlagFavorite = 0x01;
|
||||||
|
const int contactFlagTeleBase = 0x02; // 'base' permission includes battery
|
||||||
|
const int contactFlagTeleLoc = 0x04;
|
||||||
|
const int contactFlagTeleEnv = 0x08; //access environment sensors
|
||||||
const int contactPathLenOffset = 35;
|
const int 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
|
||||||
@@ -309,52 +372,44 @@ const int msgTextOffset = 38;
|
|||||||
class ParsedContactText {
|
class ParsedContactText {
|
||||||
final Uint8List senderPrefix;
|
final Uint8List senderPrefix;
|
||||||
final String text;
|
final String text;
|
||||||
|
|
||||||
const ParsedContactText({required this.senderPrefix, required this.text});
|
const ParsedContactText({required this.senderPrefix, required this.text});
|
||||||
}
|
}
|
||||||
|
|
||||||
ParsedContactText? parseContactMessageText(Uint8List frame) {
|
ParsedContactText? parseContactMessageText(Uint8List frame) {
|
||||||
if (frame.isEmpty) return null;
|
if (frame.isEmpty) return null;
|
||||||
final code = frame[0];
|
|
||||||
|
final message = BufferReader(frame);
|
||||||
|
try {
|
||||||
|
final code = message.readByte();
|
||||||
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
|
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Companion radio layout:
|
// Companion radio layout:
|
||||||
// [code][snr?][res?][res?][prefix x6][path_len][txt_type][timestamp x4][extra?][text...]
|
// [code][snr?][res?][res?][prefix x6][path_len][txt_type][timestamp x4][extra?][text...]
|
||||||
final isV3 = code == respCodeContactMsgRecvV3;
|
if (code == respCodeContactMsgRecvV3) {
|
||||||
final prefixOffset = isV3 ? 4 : 1;
|
// Skip SNR and reserved bytes in v3 layout
|
||||||
const prefixLen = 6;
|
message.skipBytes(3);
|
||||||
final txtTypeOffset = prefixOffset + prefixLen + 1;
|
|
||||||
final timestampOffset = txtTypeOffset + 1;
|
|
||||||
final baseTextOffset = timestampOffset + 4;
|
|
||||||
if (frame.length <= baseTextOffset) return null;
|
|
||||||
|
|
||||||
final flags = frame[txtTypeOffset];
|
|
||||||
final shiftedType = flags >> 2;
|
|
||||||
final rawType = flags;
|
|
||||||
final isPlain = shiftedType == txtTypePlain || rawType == txtTypePlain;
|
|
||||||
final isCli = shiftedType == txtTypeCliData || rawType == txtTypeCliData;
|
|
||||||
if (!isPlain && !isCli) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
final senderPrefix = message.readBytes(6); // public key
|
||||||
|
message.skipBytes(1); // path length
|
||||||
|
final textType = message.readByte();
|
||||||
|
message.skipBytes(4); // timestamp (4 bytes)
|
||||||
|
|
||||||
var text = readCString(
|
final shiftedType = textType >> 2;
|
||||||
frame,
|
final isSigned = shiftedType == txtTypeSigned || textType == txtTypeSigned;
|
||||||
baseTextOffset,
|
if (isSigned) {
|
||||||
frame.length - baseTextOffset,
|
// Signed messages have a 4-byte signature after the timestamp, before the text
|
||||||
).trim();
|
message.skipBytes(4);
|
||||||
if (text.isEmpty && frame.length > baseTextOffset + 4) {
|
|
||||||
text = readCString(
|
|
||||||
frame,
|
|
||||||
baseTextOffset + 4,
|
|
||||||
frame.length - (baseTextOffset + 4),
|
|
||||||
).trim();
|
|
||||||
}
|
}
|
||||||
|
final text = message.readCString();
|
||||||
if (text.isEmpty) return null;
|
if (text.isEmpty) return null;
|
||||||
|
|
||||||
final senderPrefix = frame.sublist(prefixOffset, prefixOffset + prefixLen);
|
|
||||||
return ParsedContactText(senderPrefix: senderPrefix, text: text);
|
return ParsedContactText(senderPrefix: senderPrefix, text: text);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error parsing contact message text: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to read uint32 little-endian
|
// Helper to read uint32 little-endian
|
||||||
@@ -377,18 +432,9 @@ int readInt32LE(Uint8List data, int offset) {
|
|||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to read null-terminated UTF-8 string
|
// Helper to convert uint32 to hex string
|
||||||
String readCString(Uint8List data, int offset, int maxLen) {
|
String ackHashToHex(int ackHash) {
|
||||||
int end = offset;
|
return ackHash.toRadixString(16).padLeft(8, '0');
|
||||||
while (end < offset + maxLen && end < data.length && data[end] != 0) {
|
|
||||||
end++;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return utf8.decode(data.sublist(offset, end), allowMalformed: true);
|
|
||||||
} catch (e) {
|
|
||||||
// Fallback to Latin-1 if UTF-8 decoding fails
|
|
||||||
return String.fromCharCodes(data.sublist(offset, end));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to convert public key to hex string
|
// Helper to convert public key to hex string
|
||||||
@@ -448,7 +494,7 @@ Uint8List buildSendTextMsgFrame(
|
|||||||
final writer = BufferWriter();
|
final writer = BufferWriter();
|
||||||
writer.writeByte(cmdSendTxtMsg);
|
writer.writeByte(cmdSendTxtMsg);
|
||||||
writer.writeByte(txtTypePlain);
|
writer.writeByte(txtTypePlain);
|
||||||
writer.writeByte(attempt.clamp(0, 3));
|
writer.writeByte(attempt.clamp(0, 255));
|
||||||
writer.writeUInt32LE(timestamp);
|
writer.writeUInt32LE(timestamp);
|
||||||
writer.writeBytes(recipientPubKey.sublist(0, 6));
|
writer.writeBytes(recipientPubKey.sublist(0, 6));
|
||||||
writer.writeString(text);
|
writer.writeString(text);
|
||||||
@@ -628,14 +674,17 @@ Uint8List buildResetPathFrame(Uint8List pubKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build CMD_ADD_UPDATE_CONTACT frame to set custom path
|
// Build CMD_ADD_UPDATE_CONTACT frame to set custom path
|
||||||
// Format: [cmd][pub_key x32][type][flags][path_len][path x64][name x32][timestamp x4]
|
// Format: [cmd][pub_key x32][type][flags][path_len][path x64][name x32][Lat? x4, Lon? x4][timestamp? x4]
|
||||||
Uint8List buildUpdateContactPathFrame(
|
Uint8List buildUpdateContactPathFrame(
|
||||||
Uint8List pubKey,
|
Uint8List pubKey,
|
||||||
Uint8List customPath,
|
Uint8List path,
|
||||||
int pathLen, {
|
int pathLen, {
|
||||||
int type = 1, // ADV_TYPE_CHAT
|
int type = 1, // ADV_TYPE_CHAT
|
||||||
int flags = 0,
|
int flags = 0,
|
||||||
String name = '',
|
String name = '',
|
||||||
|
double? lat,
|
||||||
|
double? lon,
|
||||||
|
DateTime? lastModified,
|
||||||
}) {
|
}) {
|
||||||
final writer = BufferWriter();
|
final writer = BufferWriter();
|
||||||
writer.writeByte(cmdAddUpdateContact);
|
writer.writeByte(cmdAddUpdateContact);
|
||||||
@@ -644,17 +693,7 @@ Uint8List buildUpdateContactPathFrame(
|
|||||||
writer.writeByte(flags);
|
writer.writeByte(flags);
|
||||||
writer.writeByte(pathLen);
|
writer.writeByte(pathLen);
|
||||||
|
|
||||||
// Path data (64 bytes, zero-padded)
|
writer.writeBytesPadded(path, maxPathSize);
|
||||||
final pathPadded = Uint8List(maxPathSize);
|
|
||||||
if (customPath.isNotEmpty && pathLen > 0) {
|
|
||||||
final copyLen = customPath.length < maxPathSize
|
|
||||||
? customPath.length
|
|
||||||
: maxPathSize;
|
|
||||||
for (int i = 0; i < copyLen; i++) {
|
|
||||||
pathPadded[i] = customPath[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
writer.writeBytes(pathPadded);
|
|
||||||
|
|
||||||
// Name (32 bytes, null-padded)
|
// Name (32 bytes, null-padded)
|
||||||
writer.writeCString(name, maxNameSize);
|
writer.writeCString(name, maxNameSize);
|
||||||
@@ -663,6 +702,27 @@ Uint8List buildUpdateContactPathFrame(
|
|||||||
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||||
writer.writeUInt32LE(timestamp);
|
writer.writeUInt32LE(timestamp);
|
||||||
|
|
||||||
|
if ((lat == null || lon == null) && lastModified != null) {
|
||||||
|
// If lat/lon not provided, write zeros
|
||||||
|
writer.writeInt32LE(0);
|
||||||
|
writer.writeInt32LE(0);
|
||||||
|
} else {
|
||||||
|
// Latitude and Longitude are expected in degrees, convert to int by multiplying by 1e6
|
||||||
|
// Latitude
|
||||||
|
final latitude = lat ?? 0.0;
|
||||||
|
writer.writeInt32LE((latitude * 1e6).round());
|
||||||
|
|
||||||
|
// Longitude
|
||||||
|
final longitude = lon ?? 0.0;
|
||||||
|
writer.writeInt32LE((longitude * 1e6).round());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastModified != null) {
|
||||||
|
// Last modified
|
||||||
|
final lastModifiedTimestamp = lastModified.millisecondsSinceEpoch ~/ 1000;
|
||||||
|
writer.writeUInt32LE(lastModifiedTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
return writer.toBytes();
|
return writer.toBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -675,16 +735,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
|
||||||
@@ -764,7 +823,7 @@ Uint8List buildSendCliCommandFrame(
|
|||||||
final writer = BufferWriter();
|
final writer = BufferWriter();
|
||||||
writer.writeByte(cmdSendTxtMsg);
|
writer.writeByte(cmdSendTxtMsg);
|
||||||
writer.writeByte(txtTypeCliData);
|
writer.writeByte(txtTypeCliData);
|
||||||
writer.writeByte(attempt.clamp(0, 3));
|
writer.writeByte(attempt.clamp(0, 255));
|
||||||
writer.writeUInt32LE(timestamp);
|
writer.writeUInt32LE(timestamp);
|
||||||
writer.writeBytes(repeaterPubKey.sublist(0, 6));
|
writer.writeBytes(repeaterPubKey.sublist(0, 6));
|
||||||
writer.writeString(command);
|
writer.writeString(command);
|
||||||
@@ -809,10 +868,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -826,20 +885,55 @@ Uint8List buildZeroHopContact(Uint8List pubKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build CMD_SET_OTHER_PARAMS frame
|
// Build CMD_SET_OTHER_PARAMS frame
|
||||||
// Format: [cmd][allowAutoAddContacts][allowTelemetryFlags][advertLocationPolicy][multiAcks]
|
// Format: [cmd][allowTelemetryFlags][advertLocationPolicy][multiAcks]
|
||||||
Uint8List buildSetOtherParamsFrame(
|
Uint8List buildSetOtherParamsFrame(
|
||||||
bool allowAutoAddContacts,
|
|
||||||
int allowTelemetryFlags,
|
int allowTelemetryFlags,
|
||||||
int advertLocationPolicy,
|
int advertLocationPolicy,
|
||||||
int multiAcks,
|
int multiAcks,
|
||||||
) {
|
) {
|
||||||
final writer = BufferWriter();
|
final writer = BufferWriter();
|
||||||
writer.writeByte(cmdSetOtherParams);
|
writer.writeByte(cmdSetOtherParams);
|
||||||
writer.writeByte(
|
//Going forward the app will just set Auto Add Contacts to disabled, and use the filter flags
|
||||||
allowAutoAddContacts ? 0x00 : 0x01,
|
//Allow Auto Add Contacts use inverted logic (0x01 = disabled, 0x00 = enabled).
|
||||||
); // Allow Auto Add Contacts
|
writer.writeByte(0x01);
|
||||||
writer.writeByte(allowTelemetryFlags); // Allow Telemetry Flags
|
writer.writeByte(allowTelemetryFlags); // Allow Telemetry Flags
|
||||||
writer.writeByte(advertLocationPolicy); // Advertisement Location Policy
|
writer.writeByte(advertLocationPolicy); // Advertisement Location Policy
|
||||||
writer.writeByte(multiAcks); // Multi Acknowledgements
|
writer.writeByte(multiAcks); // Multi Acknowledgements
|
||||||
return writer.toBytes();
|
return writer.toBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build CMD_SET_AUTO_ADD_CONFIG frame
|
||||||
|
// Format: [cmd][flags]
|
||||||
|
Uint8List buildSetAutoAddConfigFrame({
|
||||||
|
required bool autoAddChat,
|
||||||
|
required bool autoAddRepeater,
|
||||||
|
required bool autoAddRoomServer,
|
||||||
|
required bool autoAddSensor,
|
||||||
|
required bool overwriteOldest,
|
||||||
|
}) {
|
||||||
|
final writer = BufferWriter();
|
||||||
|
writer.writeByte(cmdSetAutoAddConfig);
|
||||||
|
int flags = 0;
|
||||||
|
if (autoAddChat) flags |= autoAddChatFlag;
|
||||||
|
if (autoAddRepeater) flags |= autoAddRepeaterFlag;
|
||||||
|
if (autoAddRoomServer) flags |= autoAddRoomServerFlag;
|
||||||
|
if (autoAddSensor) flags |= autoAddSensorFlag;
|
||||||
|
if (overwriteOldest) flags |= autoAddOverwriteOldestFlag;
|
||||||
|
writer.writeByte(flags);
|
||||||
|
return writer.toBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Build CMD_SEND_TELEMETRY_REQ
|
||||||
|
// Format: [cmd][reserved x3][pub_key? x32]
|
||||||
|
Uint8List buildSendTelemetryReq(Uint8List? pubKey) {
|
||||||
|
final writer = BufferWriter();
|
||||||
|
writer.writeByte(cmdSendTelemetryReq);
|
||||||
|
|
||||||
|
if (pubKey != null && pubKey.length == pubKeySize) {
|
||||||
|
writer.writeBytes(Uint8List(3)); // reserved bytes
|
||||||
|
writer.writeBytes(pubKey);
|
||||||
|
} else {
|
||||||
|
writer.writeBytes(Uint8List(4)); // reserved bytes
|
||||||
|
}
|
||||||
|
return writer.toBytes();
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,47 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
|
import '../utils/platform_info.dart';
|
||||||
|
|
||||||
class LinkHandler {
|
class LinkHandler {
|
||||||
|
/// Returns a [SelectableLinkify] on desktop or a [Linkify] on mobile.
|
||||||
|
static Widget buildLinkifyText({
|
||||||
|
required BuildContext context,
|
||||||
|
required String text,
|
||||||
|
required TextStyle style,
|
||||||
|
TextStyle? linkStyle,
|
||||||
|
}) {
|
||||||
|
final effectiveLinkStyle =
|
||||||
|
linkStyle ??
|
||||||
|
style.copyWith(
|
||||||
|
color: Colors.green,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
);
|
||||||
|
const options = LinkifyOptions(humanize: false, defaultToHttps: false);
|
||||||
|
const linkifiers = [UrlLinkifier()];
|
||||||
|
void onOpen(LinkableElement link) => handleLinkTap(context, link.url);
|
||||||
|
|
||||||
|
if (PlatformInfo.isDesktop) {
|
||||||
|
return SelectableLinkify(
|
||||||
|
text: text,
|
||||||
|
style: style,
|
||||||
|
linkStyle: effectiveLinkStyle,
|
||||||
|
options: options,
|
||||||
|
linkifiers: linkifiers,
|
||||||
|
onOpen: onOpen,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Linkify(
|
||||||
|
text: text,
|
||||||
|
style: style,
|
||||||
|
linkStyle: effectiveLinkStyle,
|
||||||
|
options: options,
|
||||||
|
linkifiers: linkifiers,
|
||||||
|
onOpen: onOpen,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static Future<void> handleLinkTap(BuildContext context, String url) async {
|
static Future<void> handleLinkTap(BuildContext context, String url) async {
|
||||||
// Show confirmation dialog
|
// Show confirmation dialog
|
||||||
final shouldOpen = await showDialog<bool>(
|
final shouldOpen = await showDialog<bool>(
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import '../models/contact.dart';
|
||||||
|
import '../connector/meshcore_protocol.dart';
|
||||||
|
|
||||||
|
class PathHelper {
|
||||||
|
static String formatPathHex(List<int> pathBytes) {
|
||||||
|
return pathBytes
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||||
|
.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
static String resolvePathNames(
|
||||||
|
List<int> pathBytes,
|
||||||
|
List<Contact> allContacts,
|
||||||
|
) {
|
||||||
|
return pathBytes
|
||||||
|
.map((b) {
|
||||||
|
final hex = b.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||||
|
final matches = allContacts
|
||||||
|
.where(
|
||||||
|
(c) =>
|
||||||
|
c.publicKey.first == b &&
|
||||||
|
(c.type == advTypeRepeater || c.type == advTypeRoom),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
if (matches.isEmpty) return hex;
|
||||||
|
if (matches.length == 1) return matches.first.name;
|
||||||
|
return matches.map((c) => c.name).join(' | ');
|
||||||
|
})
|
||||||
|
.join(' \u2192 ');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,50 @@ class ReactionInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ReactionHelper {
|
class ReactionHelper {
|
||||||
|
/// Apply a reaction to a list of messages by matching the reaction hash.
|
||||||
|
///
|
||||||
|
/// [messages] - the message list to search
|
||||||
|
/// [reactionInfo] - the parsed reaction
|
||||||
|
/// [getTimestampSecs] - extract timestamp seconds from a message
|
||||||
|
/// [getSenderName] - extract sender name for hash (null for 1:1 implicit)
|
||||||
|
/// [getMessageText] - extract message text
|
||||||
|
/// [getReactions] - extract current reactions map
|
||||||
|
/// [shouldSkip] - filter function to skip messages (e.g., skip outgoing for incoming reactions)
|
||||||
|
/// [updateMessage] - callback to update the message at index with new reactions
|
||||||
|
///
|
||||||
|
/// Returns whether a match was found.
|
||||||
|
static bool applyReaction<T>({
|
||||||
|
required List<T> messages,
|
||||||
|
required ReactionInfo reactionInfo,
|
||||||
|
required int Function(T) getTimestampSecs,
|
||||||
|
required String? Function(T) getSenderName,
|
||||||
|
required String Function(T) getMessageText,
|
||||||
|
required Map<String, int> Function(T) getReactions,
|
||||||
|
required bool Function(T) shouldSkip,
|
||||||
|
required void Function(int index, Map<String, int> newReactions)
|
||||||
|
updateMessage,
|
||||||
|
}) {
|
||||||
|
final targetHash = reactionInfo.targetHash;
|
||||||
|
for (int i = messages.length - 1; i >= 0; i--) {
|
||||||
|
final msg = messages[i];
|
||||||
|
if (shouldSkip(msg)) continue;
|
||||||
|
|
||||||
|
final msgHash = computeReactionHash(
|
||||||
|
getTimestampSecs(msg),
|
||||||
|
getSenderName(msg),
|
||||||
|
getMessageText(msg),
|
||||||
|
);
|
||||||
|
if (msgHash == targetHash) {
|
||||||
|
final currentReactions = Map<String, int>.from(getReactions(msg));
|
||||||
|
currentReactions[reactionInfo.emoji] =
|
||||||
|
(currentReactions[reactionInfo.emoji] ?? 0) + 1;
|
||||||
|
updateMessage(i, currentReactions);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static List<String>? _cachedEmojis;
|
static List<String>? _cachedEmojis;
|
||||||
|
|
||||||
/// Combined list of all reaction emojis in fixed order.
|
/// Combined list of all reaction emojis in fixed order.
|
||||||
|
|||||||
+145
-1
@@ -285,6 +285,7 @@
|
|||||||
"contacts_newGroup": "Нова група",
|
"contacts_newGroup": "Нова група",
|
||||||
"contacts_groupName": "Група",
|
"contacts_groupName": "Група",
|
||||||
"contacts_groupNameRequired": "Името на групата е задължително.",
|
"contacts_groupNameRequired": "Името на групата е задължително.",
|
||||||
|
"contacts_groupNameReserved": "Това име на група е запазено",
|
||||||
"contacts_groupAlreadyExists": "Групата \"{name}\" вече съществува.",
|
"contacts_groupAlreadyExists": "Групата \"{name}\" вече съществува.",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1606,6 +1607,8 @@
|
|||||||
"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_lastSeen": "Последно видян",
|
||||||
"snrIndicator_nearByRepeaters": "Близки повтарящи се устройства",
|
"snrIndicator_nearByRepeaters": "Близки повтарящи се устройства",
|
||||||
"chat_ShowAllPaths": "Покажи всички пътища",
|
"chat_ShowAllPaths": "Покажи всички пътища",
|
||||||
@@ -1799,5 +1802,146 @@
|
|||||||
"contacts_unread": "Непрочетено",
|
"contacts_unread": "Непрочетено",
|
||||||
"contacts_searchRepeaters": "Търсене на {number}{str} повтарящи се...",
|
"contacts_searchRepeaters": "Търсене на {number}{str} повтарящи се...",
|
||||||
"contacts_searchContactsNoNumber": "Търси контакти...",
|
"contacts_searchContactsNoNumber": "Търси контакти...",
|
||||||
"contacts_searchUsers": "Търсене на {number}{str} потребители..."
|
"contacts_searchUsers": "Търсене на {number}{str} потребители...",
|
||||||
|
"contactsSettings_title": "Настройки на контактите",
|
||||||
|
"contactsSettings_autoAddTitle": "Автоматично откриване",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Автоматично добавяне на потребители",
|
||||||
|
"contactsSettings_otherTitle": "Други настройки свързани с контакти",
|
||||||
|
"settings_contactSettingsSubtitle": "Настройки за добавяне на контакти.",
|
||||||
|
"settings_contactSettings": "Настройки за контакти",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Автоматично добавяне на датчици",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Автоматично добавяне на сървъри на стаите",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Позволи на спътника да добавя автоматично откритите сървъри на стаите.",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Автоматично добавяне на повтарящи се елементи",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Позволи на спътника да добавя автоматично откритите потребители.",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Позволи на спътника да добавя автоматично откритите повтарящи се устройства.",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Позволи на спътника да добавя автоматично откритите датчици.",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Премахни най-старото",
|
||||||
|
"discoveredContacts_Title": "Открити контакти",
|
||||||
|
"discoveredContacts_searchHint": "Търсене на открити контакти",
|
||||||
|
"discoveredContacts_noMatching": "Няма съвпадащи контакти",
|
||||||
|
"discoveredContacts_contactAdded": "Контакт добавен",
|
||||||
|
"discoveredContacts_copyContact": "Копирай контакт в клипборда",
|
||||||
|
"discoveredContacts_deleteContact": "Изтрий контакт",
|
||||||
|
"discoveredContacts_addContact": "Добави контакт",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Когато списъкът с контакти е пълен, най-старият неключов контакт ще бъде заменен.",
|
||||||
|
"discoveredContacts_deleteContactAll": "Изтриване на Всички Открити Контакти",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Сигурни ли сте, че искате да изтриете всички открити контакти?",
|
||||||
|
"common_deleteAll": "Изтрий всичко",
|
||||||
|
"map_guessedLocation": "Предполагано местоположение",
|
||||||
|
"map_showGuessedLocations": "Покажете местоположенията на предположените възли.",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenTitle": "Свържете се чрез USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenSubtitle": "Изберете открития сериен уред и свържете директно към вашия MeshCore възел.",
|
||||||
|
"usbScreenStatus": "Изберете USB устройство",
|
||||||
|
"usbScreenNote": "USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.",
|
||||||
|
"usbScreenEmptyState": "Няма открити USB устройства. Включете едно и опитайте отново.",
|
||||||
|
"usbErrorPermissionDenied": "Не беше разрешено достъпът през USB.",
|
||||||
|
"usbErrorDeviceMissing": "Избраното USB устройство вече не е налично.",
|
||||||
|
"usbErrorInvalidPort": "Изберете валитно USB устройство.",
|
||||||
|
"usbErrorBusy": "Друг мол за свързване през USB вече е в процес на изпълнение.",
|
||||||
|
"usbErrorNotConnected": "Няма свързано USB устройство.",
|
||||||
|
"usbErrorOpenFailed": "Не успях да отворя избраното USB устройство.",
|
||||||
|
"usbErrorConnectFailed": "Не успях да се свържа с избраното USB устройство.",
|
||||||
|
"usbErrorUnsupported": "USB серийната комуникация не се поддържа на тази платформа.",
|
||||||
|
"usbErrorAlreadyActive": "USB връзката вече е активирана.",
|
||||||
|
"usbErrorNoDeviceSelected": "Няма избран USB устройство.",
|
||||||
|
"usbErrorPortClosed": "USB връзката не е активна.",
|
||||||
|
"usbFallbackDeviceName": "Устройство за четене на уеб серийни данни",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_connecting": "Свързване към USB устройство...",
|
||||||
|
"usbConnectionFailed": "Неуспешно свързване през USB: {error}",
|
||||||
|
"usbStatus_notConnected": "Изберете USB устройство",
|
||||||
|
"usbStatus_searching": "Търсене на USB устройства...",
|
||||||
|
"usbErrorConnectTimedOut": "Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка.",
|
||||||
|
"@tcpStatus_connectingTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"endpoint": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@tcpConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tcpHostHint": "192.168.40.10",
|
||||||
|
"tcpScreenTitle": "Свържете се чрез TCP",
|
||||||
|
"connectionChoiceTcpLabel": "TCP",
|
||||||
|
"tcpHostLabel": "IP адрес",
|
||||||
|
"tcpPortLabel": "Пристанище",
|
||||||
|
"tcpPortHint": "5000",
|
||||||
|
"tcpStatus_notConnected": "Въведете крайната точка и свържете се.",
|
||||||
|
"tcpStatus_connectingTo": "Свързване към {endpoint}...",
|
||||||
|
"tcpErrorHostRequired": "Необходим е IP адрес.",
|
||||||
|
"tcpErrorPortInvalid": "Портът трябва да бъде между 1 и 65535.",
|
||||||
|
"tcpErrorUnsupported": "Транспортът чрез TCP не се поддържа на тази платформа.",
|
||||||
|
"tcpErrorTimedOut": "Връзката TCP изтекла.",
|
||||||
|
"tcpConnectionFailed": "Неуспешно е установено TCP връзката: {error}",
|
||||||
|
"map_showDiscoveryContacts": "Покажи контакти за откриване",
|
||||||
|
"map_setAsMyLocation": "Задайте като моя местоположение",
|
||||||
|
"@path_routeWeight": {
|
||||||
|
"placeholders": {
|
||||||
|
"weight": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"max": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings_denyAll": "Откажи всичко",
|
||||||
|
"settings_allowAll": "Позволи всичко",
|
||||||
|
"settings_allowByContact": "Позволи по флагове за контакт",
|
||||||
|
"settings_privacy": "Настройки на поверителността",
|
||||||
|
"settings_privacySettingsDescription": "Изберете каква информация устройството ви споделя с другите.",
|
||||||
|
"settings_privacySubtitle": "Контролирайте каква информация се споделя.",
|
||||||
|
"settings_telemetryBaseMode": "Базов режим на телеметрия",
|
||||||
|
"settings_telemetryLocationMode": "Режим на местоположение на телеметрията",
|
||||||
|
"settings_advertLocation": "Място на обявата",
|
||||||
|
"settings_advertLocationSubtitle": "Включи местоположение в обявата",
|
||||||
|
"contact_info": "Контактна информация",
|
||||||
|
"settings_telemetryEnvironmentMode": "Режим на средата на телеметрията",
|
||||||
|
"contact_telemetry": "Телеметрия",
|
||||||
|
"contact_lastSeen": "Последно видян",
|
||||||
|
"contact_clearChat": "Изчисти чата",
|
||||||
|
"contact_teleBase": "Базата данни за телеметрия",
|
||||||
|
"contact_settings": "Настройки за контакти",
|
||||||
|
"contact_teleBaseSubtitle": "Позволи споделяне на ниво на батерията и основна телеметрия",
|
||||||
|
"contact_teleEnv": "Среда на телеметрия",
|
||||||
|
"contact_teleLocSubtitle": "Позволи споделяне на данни за местоположение",
|
||||||
|
"contact_teleLoc": "Местоположение на телеметрията",
|
||||||
|
"contact_teleEnvSubtitle": "Позволи споделяне на данни от средносферните датчици",
|
||||||
|
"@settings_multiAck": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"appSettings_initialRouteWeight": "Първоначална тежест на маршрута",
|
||||||
|
"appSettings_maxRouteWeight": "Максимално допустимо тегло на маршрута",
|
||||||
|
"appSettings_initialRouteWeightSubtitle": "Начално тегло за новооткрити маршрути",
|
||||||
|
"appSettings_maxRouteWeightSubtitle": "Максималното тегло, което един маршрут може да събере от успешни доставки.",
|
||||||
|
"appSettings_routeWeightSuccessIncrement": "Увеличение на теглото за успех",
|
||||||
|
"appSettings_routeWeightSuccessIncrementSubtitle": "Тегло, добавено към път след успешно доставяне.",
|
||||||
|
"appSettings_routeWeightFailureDecrement": "Намаляване на теглото, свързано с неуспех",
|
||||||
|
"appSettings_routeWeightFailureDecrementSubtitle": "Тегло, което е било премахнато от пътя след неуспешен опит за доставка.",
|
||||||
|
"appSettings_maxMessageRetries": "Максимален брой опити за изпращане на съобщение",
|
||||||
|
"appSettings_maxMessageRetriesSubtitle": "Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.",
|
||||||
|
"path_routeWeight": "{weight}/{max}",
|
||||||
|
"settings_multiAck": "Мулти-потвърди: {value}",
|
||||||
|
"settings_telemetryModeUpdated": "Режим на телеметрията е обновен",
|
||||||
|
"map_showOverlaps": "Покриване на ключа на повтаряча",
|
||||||
|
"map_runTraceWithReturnPath": "Върни се по същия път."
|
||||||
}
|
}
|
||||||
+151
-7
@@ -285,6 +285,7 @@
|
|||||||
"contacts_newGroup": "Neue Gruppe",
|
"contacts_newGroup": "Neue Gruppe",
|
||||||
"contacts_groupName": "Gruppenname",
|
"contacts_groupName": "Gruppenname",
|
||||||
"contacts_groupNameRequired": "Der Gruppennamen ist erforderlich.",
|
"contacts_groupNameRequired": "Der Gruppennamen ist erforderlich.",
|
||||||
|
"contacts_groupNameReserved": "Dieser Gruppenname ist reserviert",
|
||||||
"contacts_groupAlreadyExists": "Die Gruppe \"{name}\" existiert bereits.",
|
"contacts_groupAlreadyExists": "Die Gruppe \"{name}\" existiert bereits.",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -296,8 +297,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": {
|
||||||
@@ -305,8 +306,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": {
|
||||||
@@ -314,8 +315,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": {
|
||||||
@@ -1635,6 +1636,8 @@
|
|||||||
"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_lastSeen": "Zuletzt gesehen",
|
||||||
@@ -1827,5 +1830,146 @@
|
|||||||
"contacts_searchRepeaters": "Suche {number}{str} Repeater...",
|
"contacts_searchRepeaters": "Suche {number}{str} Repeater...",
|
||||||
"contacts_searchFavorites": "Suche {number}{str} Favoriten...",
|
"contacts_searchFavorites": "Suche {number}{str} Favoriten...",
|
||||||
"contacts_searchUsers": "Suche {number}{str} Benutzer...",
|
"contacts_searchUsers": "Suche {number}{str} Benutzer...",
|
||||||
"contacts_searchRoomServers": "Suche {number}{str} Raumserver..."
|
"contacts_searchRoomServers": "Suche {number}{str} Raumserver...",
|
||||||
|
"settings_contactSettings": "Kontakteinstellungen",
|
||||||
|
"contactsSettings_otherTitle": "Weitere Einstellungen zu Kontakten",
|
||||||
|
"contactsSettings_title": "Kontakteinstellungen",
|
||||||
|
"contactsSettings_autoAddTitle": "Automatische Erkennung",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Automatische Hinzufügung von Benutzern",
|
||||||
|
"settings_contactSettingsSubtitle": "Einstellungen für das Hinzufügen von Kontakten",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Automatisch Sensoren hinzufügen",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Benutzer hinzuzufügen",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Automatisch Raumservers hinzufügen",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Ermöglichen Sie dem Begleiter, entdeckte Raumserver automatisch hinzuzufügen",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Automatisch Repeater hinzufügen",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Repeater hinzuzufügen.",
|
||||||
|
"discoveredContacts_noMatching": "Keine passenden Kontakte",
|
||||||
|
"discoveredContacts_searchHint": "Entdeckte Kontakte suchen",
|
||||||
|
"discoveredContacts_addContact": "Kontakt hinzufügen",
|
||||||
|
"discoveredContacts_contactAdded": "Kontakt hinzugefügt",
|
||||||
|
"discoveredContacts_deleteContact": "Kontakt löschen",
|
||||||
|
"discoveredContacts_Title": "Entdeckte Kontakte",
|
||||||
|
"discoveredContacts_copyContact": "Kontakt in die Zwischenablage kopieren",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Überschreiben des Ältesten",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Sensoren hinzuzufügen",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Wenn die Kontaktliste voll ist, wird der älteste nicht favorisierte Kontakt ersetzt.",
|
||||||
|
"common_deleteAll": "Alles löschen",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Sind Sie sicher, dass Sie alle gefundenen Kontakte löschen möchten?",
|
||||||
|
"discoveredContacts_deleteContactAll": "Alle entdeckten Kontakte löschen",
|
||||||
|
"map_showGuessedLocations": "Zeige die vermuteten Knotenpositionen",
|
||||||
|
"map_guessedLocation": "Geschätzter Ort",
|
||||||
|
"usbScreenSubtitle": "Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenTitle": "Verbinden über USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenStatus": "Wählen Sie ein USB-Gerät aus",
|
||||||
|
"usbScreenNote": "Die USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.",
|
||||||
|
"usbScreenEmptyState": "Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie.",
|
||||||
|
"usbErrorPermissionDenied": "Die USB-Berechtigung wurde abgelehnt.",
|
||||||
|
"usbErrorDeviceMissing": "Das ausgewählte USB-Gerät ist nicht mehr verfügbar.",
|
||||||
|
"usbErrorInvalidPort": "Wählen Sie ein gültiges USB-Gerät aus.",
|
||||||
|
"usbErrorBusy": "Eine weitere Anfrage für eine USB-Verbindung ist bereits in Bearbeitung.",
|
||||||
|
"usbErrorNotConnected": "Es ist kein USB-Gerät angeschlossen.",
|
||||||
|
"usbErrorOpenFailed": "Fehlgeschlagen beim Öffnen des ausgewählten USB-Geräts.",
|
||||||
|
"usbErrorConnectFailed": "Keine Verbindung zum ausgewählten USB-Gerät hergestellt.",
|
||||||
|
"usbErrorUnsupported": "Die USB-Serielle Schnittstelle wird auf dieser Plattform nicht unterstützt.",
|
||||||
|
"usbErrorAlreadyActive": "Eine USB-Verbindung ist bereits hergestellt.",
|
||||||
|
"usbErrorNoDeviceSelected": "Kein USB-Gerät wurde ausgewählt.",
|
||||||
|
"usbErrorPortClosed": "Die USB-Verbindung ist nicht aktiv.",
|
||||||
|
"usbFallbackDeviceName": "Web-Serielle Geräte",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_searching": "Suche nach USB-Geräten...",
|
||||||
|
"usbStatus_notConnected": "Wählen Sie ein USB-Gerät aus",
|
||||||
|
"usbStatus_connecting": "Verbindung zum USB-Gerät...",
|
||||||
|
"usbConnectionFailed": "Fehler beim USB-Verbindungsaufbau: {error}",
|
||||||
|
"usbErrorConnectTimedOut": "Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält.",
|
||||||
|
"@tcpStatus_connectingTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"endpoint": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@tcpConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tcpHostLabel": "IP-Adresse",
|
||||||
|
"connectionChoiceTcpLabel": "TCP",
|
||||||
|
"tcpHostHint": "192.168.40.10",
|
||||||
|
"tcpScreenTitle": "Verbinden über TCP",
|
||||||
|
"tcpPortLabel": "Port",
|
||||||
|
"tcpPortHint": "5000",
|
||||||
|
"tcpStatus_notConnected": "Geben Sie den Endpunkt ein und verbinden Sie sich.",
|
||||||
|
"tcpStatus_connectingTo": "Verbindung zu {endpoint}...",
|
||||||
|
"tcpErrorHostRequired": "Eine IP-Adresse ist erforderlich.",
|
||||||
|
"tcpErrorPortInvalid": "Die Portnummer muss zwischen 1 und 65535 liegen.",
|
||||||
|
"tcpErrorUnsupported": "Die TCP-Übertragung wird auf dieser Plattform nicht unterstützt.",
|
||||||
|
"tcpErrorTimedOut": "Die TCP-Verbindung ist abgelaufen.",
|
||||||
|
"tcpConnectionFailed": "Fehler beim TCP-Verbindungsaufbau: {error}",
|
||||||
|
"map_showDiscoveryContacts": "Entdeckungs-Kontakte anzeigen",
|
||||||
|
"map_setAsMyLocation": "Als meine aktuelle Position festlegen",
|
||||||
|
"@path_routeWeight": {
|
||||||
|
"placeholders": {
|
||||||
|
"weight": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"max": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings_allowByContact": "Zulassen durch Kontaktflaggen",
|
||||||
|
"settings_privacy": "Datenschutzeinstellungen",
|
||||||
|
"settings_allowAll": "Alles zulassen",
|
||||||
|
"settings_privacySettingsDescription": "Wählen Sie die Informationen, die Ihr Gerät mit anderen teilt.",
|
||||||
|
"settings_denyAll": "Alle ablehnen",
|
||||||
|
"settings_privacySubtitle": "Steuern Sie die Informationen, die freigegeben werden.",
|
||||||
|
"settings_telemetryLocationMode": "Telemetrie-Ortsmodus",
|
||||||
|
"settings_telemetryEnvironmentMode": "Telemetrie-Umgebungsmodus",
|
||||||
|
"settings_advertLocation": "Anzeigenort",
|
||||||
|
"settings_advertLocationSubtitle": "Ort in der Anzeige einbeziehen",
|
||||||
|
"settings_telemetryBaseMode": "Telemetrie-Basismodus",
|
||||||
|
"contact_teleBase": "Telemetriebasis",
|
||||||
|
"contact_teleBaseSubtitle": "Erlauben des Freigebens des Batteriestands und der grundlegenden Telemetrie",
|
||||||
|
"contact_teleLoc": "Telemetrieort",
|
||||||
|
"contact_teleLocSubtitle": "Teilen von Standortdaten zulassen",
|
||||||
|
"contact_info": "Kontaktinformationen",
|
||||||
|
"contact_settings": "Kontakteinstellungen",
|
||||||
|
"contact_telemetry": "Telemetrie",
|
||||||
|
"contact_teleEnv": "Telemetrieumgebung",
|
||||||
|
"contact_lastSeen": "Zuletzt gesehen",
|
||||||
|
"contact_clearChat": "Chat löschen",
|
||||||
|
"contact_teleEnvSubtitle": "Teilen von Umgebungsensordaten zulassen",
|
||||||
|
"@settings_multiAck": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"appSettings_initialRouteWeightSubtitle": "Ausgangsgewicht für neu entdeckte Pfade",
|
||||||
|
"appSettings_maxRouteWeightSubtitle": "Maximales Gewicht, das ein Weg durch erfolgreiche Lieferungen erreichen kann.",
|
||||||
|
"appSettings_maxRouteWeight": "Maximale Gesamtstreckenlänge",
|
||||||
|
"appSettings_initialRouteWeight": "Anfangs-Streckengewicht",
|
||||||
|
"appSettings_routeWeightSuccessIncrement": "Erhöhung des Erfolgsgewichts",
|
||||||
|
"appSettings_routeWeightSuccessIncrementSubtitle": "Gewicht, das einem Pfad nach erfolgreicher Lieferung hinzugefügt wird.",
|
||||||
|
"appSettings_routeWeightFailureDecrement": "Reduzierung des Gewichts bei Fehlern",
|
||||||
|
"appSettings_routeWeightFailureDecrementSubtitle": "Gewicht, das nach einem fehlgeschlagenen Versand von einem Weg entfernt wurde",
|
||||||
|
"appSettings_maxMessageRetries": "Maximale Anzahl an Wiederholungsversuchen",
|
||||||
|
"appSettings_maxMessageRetriesSubtitle": "Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.",
|
||||||
|
"path_routeWeight": "{weight}/{max}",
|
||||||
|
"settings_telemetryModeUpdated": "Telemetriemodus aktualisiert",
|
||||||
|
"settings_multiAck": "Mehrfach-Bestätigungen: {value}",
|
||||||
|
"map_showOverlaps": "Überlappungen der Repeater-Taste",
|
||||||
|
"map_runTraceWithReturnPath": "Auf dem gleichen Pfad zurückkehren."
|
||||||
}
|
}
|
||||||
+148
-8
@@ -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,64 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scanner_title": "MeshCore Open",
|
"scanner_title": "MeshCore Open",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"connectionChoiceTcpLabel": "TCP",
|
||||||
|
"tcpScreenTitle": "Connect over TCP",
|
||||||
|
"tcpHostLabel": "IP Address",
|
||||||
|
"tcpHostHint": "192.168.40.10",
|
||||||
|
"tcpPortLabel": "Port",
|
||||||
|
"tcpPortHint": "5000",
|
||||||
|
"tcpStatus_notConnected": "Enter endpoint and connect",
|
||||||
|
"tcpStatus_connectingTo": "Connecting to {endpoint}...",
|
||||||
|
"@tcpStatus_connectingTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"endpoint": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tcpErrorHostRequired": "IP address is required.",
|
||||||
|
"tcpErrorPortInvalid": "Port must be between 1 and 65535.",
|
||||||
|
"tcpErrorUnsupported": "TCP transport is not supported on this platform.",
|
||||||
|
"tcpErrorTimedOut": "TCP connection timed out.",
|
||||||
|
"tcpConnectionFailed": "TCP connection failed: {error}",
|
||||||
|
"@tcpConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbScreenTitle": "Connect over USB",
|
||||||
|
"usbScreenSubtitle": "Choose a detected serial device and connect directly to your MeshCore node.",
|
||||||
|
"usbScreenStatus": "Select a USB device",
|
||||||
|
"usbScreenNote": "USB serial is active on supported Android devices and desktop platforms.",
|
||||||
|
"usbScreenEmptyState": "No USB devices found. Plug one in and refresh.",
|
||||||
|
"usbErrorPermissionDenied": "USB permission was denied.",
|
||||||
|
"usbErrorDeviceMissing": "The selected USB device is no longer available.",
|
||||||
|
"usbErrorInvalidPort": "Select a valid USB device.",
|
||||||
|
"usbErrorBusy": "Another USB connection request is already in progress.",
|
||||||
|
"usbErrorNotConnected": "No USB device is connected.",
|
||||||
|
"usbErrorOpenFailed": "Failed to open the selected USB device.",
|
||||||
|
"usbErrorConnectFailed": "Failed to connect to the selected USB device.",
|
||||||
|
"usbErrorUnsupported": "USB serial is not supported on this platform.",
|
||||||
|
"usbErrorAlreadyActive": "A USB connection is already active.",
|
||||||
|
"usbErrorNoDeviceSelected": "No USB device was selected.",
|
||||||
|
"usbErrorPortClosed": "The USB connection is not open.",
|
||||||
|
"usbErrorConnectTimedOut": "Connection timed out. Make sure the device has USB Companion firmware.",
|
||||||
|
"usbFallbackDeviceName": "Web Serial Device",
|
||||||
|
"usbStatus_notConnected": "Select a USB device",
|
||||||
|
"usbStatus_connecting": "Connecting to USB device...",
|
||||||
|
"usbStatus_searching": "Searching for USB devices...",
|
||||||
|
"usbConnectionFailed": "USB connection failed: {error}",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"scanner_scanning": "Scanning for devices...",
|
"scanner_scanning": "Scanning for devices...",
|
||||||
"scanner_connecting": "Connecting...",
|
"scanner_connecting": "Connecting...",
|
||||||
"scanner_disconnecting": "Disconnecting...",
|
"scanner_disconnecting": "Disconnecting...",
|
||||||
@@ -72,6 +131,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,11 +159,33 @@
|
|||||||
"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.",
|
||||||
"settings_privacyModeEnabled": "Privacy mode enabled",
|
"settings_privacyModeEnabled": "Privacy mode enabled",
|
||||||
"settings_privacyModeDisabled": "Privacy mode disabled",
|
"settings_privacyModeDisabled": "Privacy mode disabled",
|
||||||
|
"settings_privacy": "Privacy Settings",
|
||||||
|
"settings_privacySubtitle": "Control what information is shared.",
|
||||||
|
"settings_privacySettingsDescription": "Choose what information your device shares with others.",
|
||||||
|
"settings_denyAll": "Deny all",
|
||||||
|
"settings_allowByContact": "Allow by contact flags",
|
||||||
|
"settings_allowAll": "Allow all",
|
||||||
|
"settings_telemetryBaseMode": "Telemetry Base Mode",
|
||||||
|
"settings_telemetryLocationMode": "Telemetry Location Mode",
|
||||||
|
"settings_telemetryEnvironmentMode": "Telemetry Environment Mode",
|
||||||
|
"settings_advertLocation": "Advert Location",
|
||||||
|
"settings_advertLocationSubtitle": "Include location in advert.",
|
||||||
|
"settings_multiAck": "Multi-ACKs: {value}",
|
||||||
|
"@settings_multiAck": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings_telemetryModeUpdated": "Telemetry mode updated",
|
||||||
"settings_actions": "Actions",
|
"settings_actions": "Actions",
|
||||||
"settings_sendAdvertisement": "Send Advertisement",
|
"settings_sendAdvertisement": "Send Advertisement",
|
||||||
"settings_sendAdvertisementSubtitle": "Broadcast presence now",
|
"settings_sendAdvertisementSubtitle": "Broadcast presence now",
|
||||||
@@ -206,6 +289,23 @@
|
|||||||
"appSettings_autoRouteRotationSubtitle": "Cycle between best paths and flood mode",
|
"appSettings_autoRouteRotationSubtitle": "Cycle between best paths and flood mode",
|
||||||
"appSettings_autoRouteRotationEnabled": "Auto route rotation enabled",
|
"appSettings_autoRouteRotationEnabled": "Auto route rotation enabled",
|
||||||
"appSettings_autoRouteRotationDisabled": "Auto route rotation disabled",
|
"appSettings_autoRouteRotationDisabled": "Auto route rotation disabled",
|
||||||
|
"appSettings_maxRouteWeight": "Max Route Weight",
|
||||||
|
"appSettings_maxRouteWeightSubtitle": "Maximum weight a path can accumulate from successful deliveries",
|
||||||
|
"appSettings_initialRouteWeight": "Initial Route Weight",
|
||||||
|
"appSettings_initialRouteWeightSubtitle": "Starting weight for newly discovered paths",
|
||||||
|
"appSettings_routeWeightSuccessIncrement": "Success Weight Increment",
|
||||||
|
"appSettings_routeWeightSuccessIncrementSubtitle": "Weight added to a path after successful delivery",
|
||||||
|
"appSettings_routeWeightFailureDecrement": "Failure Weight Decrement",
|
||||||
|
"appSettings_routeWeightFailureDecrementSubtitle": "Weight removed from a path after failed delivery",
|
||||||
|
"appSettings_maxMessageRetries": "Max Message Retries",
|
||||||
|
"appSettings_maxMessageRetriesSubtitle": "Number of retry attempts before marking a message as failed",
|
||||||
|
"path_routeWeight": "{weight}/{max}",
|
||||||
|
"@path_routeWeight": {
|
||||||
|
"placeholders": {
|
||||||
|
"weight": { "type": "String" },
|
||||||
|
"max": { "type": "String" }
|
||||||
|
}
|
||||||
|
},
|
||||||
"appSettings_battery": "Battery",
|
"appSettings_battery": "Battery",
|
||||||
"appSettings_batteryChemistry": "Battery Chemistry",
|
"appSettings_batteryChemistry": "Battery Chemistry",
|
||||||
"appSettings_batteryChemistryPerDevice": "Set per device ({deviceName})",
|
"appSettings_batteryChemistryPerDevice": "Set per device ({deviceName})",
|
||||||
@@ -353,6 +453,7 @@
|
|||||||
"contacts_newGroup": "New Group",
|
"contacts_newGroup": "New Group",
|
||||||
"contacts_groupName": "Group name",
|
"contacts_groupName": "Group name",
|
||||||
"contacts_groupNameRequired": "Group name is required",
|
"contacts_groupNameRequired": "Group name is required",
|
||||||
|
"contacts_groupNameReserved": "This group name is reserved",
|
||||||
"contacts_groupAlreadyExists": "Group \"{name}\" already exists",
|
"contacts_groupAlreadyExists": "Group \"{name}\" already exists",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -364,8 +465,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": {
|
||||||
@@ -373,8 +474,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": {
|
||||||
@@ -382,8 +483,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": {
|
||||||
@@ -391,6 +492,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"contact_info": "Contact Info",
|
||||||
|
"contact_settings": "Contact Settings",
|
||||||
|
"contact_telemetry": "Telemetry",
|
||||||
|
"contact_lastSeen": "Last seen",
|
||||||
|
"contact_clearChat": "Clear Chat",
|
||||||
|
"contact_teleBase": "Telemetry Base",
|
||||||
|
"contact_teleBaseSubtitle": "Allow sharing battery level and basic telemetry",
|
||||||
|
"contact_teleLoc": "Telemetry Location",
|
||||||
|
"contact_teleLocSubtitle": "Allow sharing location data",
|
||||||
|
"contact_teleEnv": "Telemetry Environment",
|
||||||
|
"contact_teleEnvSubtitle": "Allow sharing environment sensor data",
|
||||||
"channels_title": "Channels",
|
"channels_title": "Channels",
|
||||||
"channels_noChannelsConfigured": "No channels configured",
|
"channels_noChannelsConfigured": "No channels configured",
|
||||||
"channels_addPublicChannel": "Add Public Channel",
|
"channels_addPublicChannel": "Add Public Channel",
|
||||||
@@ -744,6 +856,7 @@
|
|||||||
"map_source": "Source",
|
"map_source": "Source",
|
||||||
"map_flags": "Flags",
|
"map_flags": "Flags",
|
||||||
"map_shareMarkerHere": "Share marker here",
|
"map_shareMarkerHere": "Share marker here",
|
||||||
|
"map_setAsMyLocation": "Set as my location",
|
||||||
"map_pinLabel": "Pin label",
|
"map_pinLabel": "Pin label",
|
||||||
"map_label": "Label",
|
"map_label": "Label",
|
||||||
"map_pointOfInterest": "Point of interest",
|
"map_pointOfInterest": "Point of interest",
|
||||||
@@ -765,17 +878,22 @@
|
|||||||
"map_chatNodes": "Chat Nodes",
|
"map_chatNodes": "Chat Nodes",
|
||||||
"map_repeaters": "Repeaters",
|
"map_repeaters": "Repeaters",
|
||||||
"map_otherNodes": "Other Nodes",
|
"map_otherNodes": "Other Nodes",
|
||||||
|
"map_showOverlaps": "Repeater Key Overlaps",
|
||||||
"map_keyPrefix": "Key Prefix",
|
"map_keyPrefix": "Key Prefix",
|
||||||
"map_filterByKeyPrefix": "Filter by key prefix",
|
"map_filterByKeyPrefix": "Filter by key prefix",
|
||||||
"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_showDiscoveryContacts": "Show Discovery Contacts",
|
||||||
|
"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",
|
||||||
"map_manageRepeater": "Manage Repeater",
|
"map_manageRepeater": "Manage Repeater",
|
||||||
"map_tapToAdd": "Tap on nodes to add them to the path.",
|
"map_tapToAdd": "Tap on nodes to add them to the path.",
|
||||||
"map_runTrace": "Run Path Trace",
|
"map_runTrace": "Run path trace",
|
||||||
|
"map_runTraceWithReturnPath": "Return back on the same path.",
|
||||||
"map_removeLast": "Remove Last",
|
"map_removeLast": "Remove Last",
|
||||||
"map_pathTraceCancelled": "Path trace cancelled.",
|
"map_pathTraceCancelled": "Path trace cancelled.",
|
||||||
"mapCache_title": "Offline Map Cache",
|
"mapCache_title": "Offline Map Cache",
|
||||||
@@ -1837,5 +1955,27 @@
|
|||||||
"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_nearByRepeaters": "Nearby Repeaters",
|
||||||
"snrIndicator_lastSeen": "Last seen"
|
"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?"
|
||||||
}
|
}
|
||||||
+150
-6
@@ -285,6 +285,7 @@
|
|||||||
"contacts_newGroup": "Nuevo Grupo",
|
"contacts_newGroup": "Nuevo Grupo",
|
||||||
"contacts_groupName": "Nombre del grupo",
|
"contacts_groupName": "Nombre del grupo",
|
||||||
"contacts_groupNameRequired": "El nombre del grupo es obligatorio",
|
"contacts_groupNameRequired": "El nombre del grupo es obligatorio",
|
||||||
|
"contacts_groupNameReserved": "Este nombre de grupo está reservado",
|
||||||
"contacts_groupAlreadyExists": "El grupo \"{name}\" ya existe",
|
"contacts_groupAlreadyExists": "El grupo \"{name}\" ya existe",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -297,7 +298,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": {
|
||||||
@@ -305,8 +306,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": {
|
||||||
@@ -314,8 +315,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": {
|
||||||
@@ -1632,6 +1633,8 @@
|
|||||||
"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_nearByRepeaters": "Repetidores cercanos",
|
||||||
@@ -1827,5 +1830,146 @@
|
|||||||
"contacts_searchFavorites": "Buscar {number}{str} Favoritos...",
|
"contacts_searchFavorites": "Buscar {number}{str} Favoritos...",
|
||||||
"contacts_searchUsers": "Buscar {number}{str} Usuarios...",
|
"contacts_searchUsers": "Buscar {number}{str} Usuarios...",
|
||||||
"contacts_searchRepeaters": "Buscar {number}{str} Repetidores...",
|
"contacts_searchRepeaters": "Buscar {number}{str} Repetidores...",
|
||||||
"contacts_searchRoomServers": "Buscar {number}{str} servidores de sala..."
|
"contacts_searchRoomServers": "Buscar {number}{str} servidores de sala...",
|
||||||
|
"contactsSettings_autoAddTitle": "Detección automática",
|
||||||
|
"settings_contactSettings": "Configuración de contacto",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Agregar usuarios automáticamente",
|
||||||
|
"contactsSettings_otherTitle": "Otras configuraciones relacionadas con el contacto",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Permitir que el compañero agregue automáticamente a los usuarios descubiertos.",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Permitir que el compañero agregue automáticamente los repetidores descubiertos.",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Permitir que el compañero agregue automáticamente los servidores de salas descubiertos.",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Agregar sensores automáticamente",
|
||||||
|
"contactsSettings_title": "Configuración de contactos",
|
||||||
|
"settings_contactSettingsSubtitle": "Configuración de cómo se agregan los contactos.",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Permitir que el compañero agregue automáticamente los sensores descubiertos.",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Agregar repetidores automáticamente",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Sobreescribir el más antiguo",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Agregar automáticamente servidores de sala",
|
||||||
|
"discoveredContacts_noMatching": "No se encontraron contactos coincidentes",
|
||||||
|
"discoveredContacts_contactAdded": "Contacto agregado",
|
||||||
|
"discoveredContacts_copyContact": "Copiar contacto al portapapeles",
|
||||||
|
"discoveredContacts_deleteContact": "Eliminar contacto",
|
||||||
|
"discoveredContacts_Title": "Contactos descubiertos",
|
||||||
|
"discoveredContacts_searchHint": "Buscar contactos descubiertos",
|
||||||
|
"discoveredContacts_addContact": "Agregar contacto",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Cuando la lista de contactos esté llena, se reemplazará el contacto no favorito más antiguo.",
|
||||||
|
"common_deleteAll": "Eliminar todo",
|
||||||
|
"discoveredContacts_deleteContactAll": "Eliminar Todos los Contactos Descubiertos",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "¿Está seguro de que desea eliminar todos los contactos descubiertos!",
|
||||||
|
"map_guessedLocation": "Ubicación estimada",
|
||||||
|
"map_showGuessedLocations": "Mostrar las ubicaciones estimadas de los nodos.",
|
||||||
|
"usbScreenTitle": "Conecte mediante USB",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenSubtitle": "Seleccione el dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.",
|
||||||
|
"usbScreenStatus": "Seleccione un dispositivo USB",
|
||||||
|
"usbScreenNote": "La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.",
|
||||||
|
"usbScreenEmptyState": "No se encontraron dispositivos USB. Conecte uno y vuelva a intentar.",
|
||||||
|
"usbErrorPermissionDenied": "Se denegó el permiso de acceso a través de USB.",
|
||||||
|
"usbErrorDeviceMissing": "El dispositivo USB seleccionado ya no está disponible.",
|
||||||
|
"usbErrorInvalidPort": "Seleccione un dispositivo USB válido.",
|
||||||
|
"usbErrorBusy": "Ya se ha iniciado una solicitud de conexión USB adicional.",
|
||||||
|
"usbErrorNotConnected": "No hay ningún dispositivo USB conectado.",
|
||||||
|
"usbErrorOpenFailed": "No se pudo abrir el dispositivo USB seleccionado.",
|
||||||
|
"usbErrorConnectFailed": "No se pudo conectar con el dispositivo USB seleccionado.",
|
||||||
|
"usbErrorUnsupported": "La comunicación serial a través de USB no está soportada en esta plataforma.",
|
||||||
|
"usbErrorAlreadyActive": "La conexión USB ya está activa.",
|
||||||
|
"usbErrorNoDeviceSelected": "No se ha seleccionado ningún dispositivo USB.",
|
||||||
|
"usbErrorPortClosed": "La conexión USB no está activa.",
|
||||||
|
"usbFallbackDeviceName": "Dispositivo de serie web",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_connecting": "Conectándose al dispositivo USB...",
|
||||||
|
"usbStatus_searching": "Buscando dispositivos USB...",
|
||||||
|
"usbStatus_notConnected": "Seleccione un dispositivo USB",
|
||||||
|
"usbConnectionFailed": "Error al conectar mediante USB: {error}",
|
||||||
|
"usbErrorConnectTimedOut": "La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion.",
|
||||||
|
"@tcpStatus_connectingTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"endpoint": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@tcpConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tcpScreenTitle": "Establecer conexión a través de TCP",
|
||||||
|
"connectionChoiceTcpLabel": "TCP",
|
||||||
|
"tcpHostHint": "192.168.40.10",
|
||||||
|
"tcpHostLabel": "Dirección IP",
|
||||||
|
"tcpPortLabel": "Puerto",
|
||||||
|
"tcpPortHint": "5000",
|
||||||
|
"tcpStatus_notConnected": "Ingrese la dirección final y conecte.",
|
||||||
|
"tcpStatus_connectingTo": "Conectándose a {endpoint}...",
|
||||||
|
"tcpErrorHostRequired": "Se requiere la dirección IP.",
|
||||||
|
"tcpErrorPortInvalid": "El puerto debe estar entre 1 y 65535.",
|
||||||
|
"tcpErrorUnsupported": "El protocolo de transporte TCP no está soportado en esta plataforma.",
|
||||||
|
"tcpErrorTimedOut": "La conexión TCP ha caducado.",
|
||||||
|
"tcpConnectionFailed": "Error en la conexión TCP: {error}",
|
||||||
|
"map_showDiscoveryContacts": "Mostrar Contactos de Descubrimiento",
|
||||||
|
"map_setAsMyLocation": "Establecer mi ubicación",
|
||||||
|
"@path_routeWeight": {
|
||||||
|
"placeholders": {
|
||||||
|
"weight": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"max": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings_privacySubtitle": "Controlar qué información se comparte.",
|
||||||
|
"settings_allowByContact": "Permitir por banderas de contacto",
|
||||||
|
"settings_denyAll": "Denegar todo",
|
||||||
|
"settings_telemetryBaseMode": "Modo base de telemetría",
|
||||||
|
"settings_telemetryEnvironmentMode": "Modo de entorno de telemetría",
|
||||||
|
"settings_advertLocationSubtitle": "Incluir ubicación en anuncio",
|
||||||
|
"contact_info": "Información de contacto",
|
||||||
|
"settings_privacySettingsDescription": "Elige qué información comparte tu dispositivo con otros.",
|
||||||
|
"settings_allowAll": "Permitir todo",
|
||||||
|
"settings_privacy": "Configuración de privacidad",
|
||||||
|
"contact_settings": "Configuración de contacto",
|
||||||
|
"settings_telemetryLocationMode": "Modo de ubicación de telemetría",
|
||||||
|
"contact_teleBase": "Base de Telemetría",
|
||||||
|
"contact_teleLoc": "Ubicación de telemetría",
|
||||||
|
"settings_advertLocation": "Ubicación de anuncio",
|
||||||
|
"contact_teleLocSubtitle": "Permitir el intercambio de datos de ubicación",
|
||||||
|
"contact_clearChat": "Borrar chat",
|
||||||
|
"contact_telemetry": "Telemetría",
|
||||||
|
"contact_lastSeen": "Visto por última vez",
|
||||||
|
"contact_teleBaseSubtitle": "Permitir el intercambio de nivel de batería y telemetría básica",
|
||||||
|
"contact_teleEnv": "Entorno de Telemetría",
|
||||||
|
"contact_teleEnvSubtitle": "Permitir el intercambio de datos de sensores de entorno",
|
||||||
|
"@settings_multiAck": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"appSettings_initialRouteWeight": "Peso inicial de la ruta",
|
||||||
|
"appSettings_maxRouteWeight": "Peso máximo permitido para la ruta",
|
||||||
|
"appSettings_initialRouteWeightSubtitle": "Peso inicial para rutas recién descubiertas",
|
||||||
|
"appSettings_maxRouteWeightSubtitle": "Peso máximo que una ruta puede acumular gracias a entregas exitosas.",
|
||||||
|
"appSettings_routeWeightSuccessIncrement": "Incremento de peso para el éxito",
|
||||||
|
"appSettings_routeWeightSuccessIncrementSubtitle": "Peso añadido a una ruta después de una entrega exitosa.",
|
||||||
|
"appSettings_routeWeightFailureDecrement": "Reducción del peso asociado al fallo",
|
||||||
|
"appSettings_routeWeightFailureDecrementSubtitle": "Peso retirado de un camino después de un intento de entrega fallido.",
|
||||||
|
"appSettings_maxMessageRetries": "Número máximo de reintentos de envío de mensajes",
|
||||||
|
"appSettings_maxMessageRetriesSubtitle": "Número de intentos de reintento antes de marcar un mensaje como fallido.",
|
||||||
|
"path_routeWeight": "{weight}/{max}",
|
||||||
|
"settings_telemetryModeUpdated": "Modo de telemetría actualizado",
|
||||||
|
"settings_multiAck": "Multi-ACKs: {value}",
|
||||||
|
"map_showOverlaps": "Superposiciones de tecla repetidora",
|
||||||
|
"map_runTraceWithReturnPath": "Volver atrás por el mismo camino."
|
||||||
}
|
}
|
||||||
+150
-6
@@ -285,6 +285,7 @@
|
|||||||
"contacts_newGroup": "Nouveau Groupe",
|
"contacts_newGroup": "Nouveau Groupe",
|
||||||
"contacts_groupName": "Nom du groupe",
|
"contacts_groupName": "Nom du groupe",
|
||||||
"contacts_groupNameRequired": "Le nom du groupe est obligatoire.",
|
"contacts_groupNameRequired": "Le nom du groupe est obligatoire.",
|
||||||
|
"contacts_groupNameReserved": "Ce nom de groupe est réservé",
|
||||||
"contacts_groupAlreadyExists": "Le groupe \"{name}\" existe déjà.",
|
"contacts_groupAlreadyExists": "Le groupe \"{name}\" existe déjà.",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -297,7 +298,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": {
|
||||||
@@ -305,8 +306,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": {
|
||||||
@@ -314,8 +315,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": {
|
||||||
@@ -1604,6 +1605,8 @@
|
|||||||
"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_lastSeen": "Dernière fois vu",
|
||||||
@@ -1799,5 +1802,146 @@
|
|||||||
"contacts_searchUsers": "Rechercher {number}{str} utilisateurs...",
|
"contacts_searchUsers": "Rechercher {number}{str} utilisateurs...",
|
||||||
"contacts_searchRoomServers": "Rechercher {number}{str} serveurs de salle...",
|
"contacts_searchRoomServers": "Rechercher {number}{str} serveurs de salle...",
|
||||||
"contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...",
|
"contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...",
|
||||||
"contacts_searchContactsNoNumber": "Rechercher des contacts..."
|
"contacts_searchContactsNoNumber": "Rechercher des contacts...",
|
||||||
|
"settings_contactSettings": "Paramètres de contact",
|
||||||
|
"settings_contactSettingsSubtitle": "Paramètres pour l'ajout de contacts",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Ajouter automatiquement les répéteurs",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Autoriser le compagnon à ajouter automatiquement les répéteurs découverts",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Ajouter automatiquement les serveurs de salle",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Autoriser le compagnon à ajouter automatiquement les serveurs de salles découverts",
|
||||||
|
"contactsSettings_otherTitle": "Autres paramètres liés aux contacts",
|
||||||
|
"contactsSettings_title": "Paramètres des contacts",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Ajouter automatiquement les utilisateurs",
|
||||||
|
"contactsSettings_autoAddTitle": "Découverte automatique",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Ajouter automatiquement les capteurs",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Autoriser le compagnon à ajouter automatiquement les utilisateurs découverts",
|
||||||
|
"discoveredContacts_noMatching": "Aucun contact correspondant",
|
||||||
|
"discoveredContacts_contactAdded": "Contact ajouté",
|
||||||
|
"discoveredContacts_addContact": "Ajouter un contact",
|
||||||
|
"discoveredContacts_copyContact": "Copier le contact dans le presse-papiers",
|
||||||
|
"discoveredContacts_deleteContact": "Supprimer le contact",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Écraser le plus ancien",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Autoriser le compagnon à ajouter automatiquement les capteurs découverts.",
|
||||||
|
"discoveredContacts_Title": "Contacts découverts",
|
||||||
|
"discoveredContacts_searchHint": "Rechercher des contacts découverts",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Lorsque la liste de contacts est pleine, le contact le plus ancien non favori sera remplacé.",
|
||||||
|
"common_deleteAll": "Supprimer tout",
|
||||||
|
"discoveredContacts_deleteContactAll": "Supprimer tous les contacts découverts",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Êtes-vous sûr de vouloir supprimer tous les contacts découverts ?",
|
||||||
|
"map_showGuessedLocations": "Afficher les emplacements des nœuds estimés",
|
||||||
|
"map_guessedLocation": "Lieu deviné",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenTitle": "Connectez via USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenSubtitle": "Sélectionnez un périphérique série détecté et connectez-vous directement à votre nœud MeshCore.",
|
||||||
|
"usbScreenStatus": "Sélectionnez un périphérique USB",
|
||||||
|
"usbScreenNote": "La communication série USB est active sur les appareils Android et les plateformes de bureau compatibles.",
|
||||||
|
"usbScreenEmptyState": "Aucun périphérique USB n'a été trouvé. Veuillez en brancher un et rafraîchir la page.",
|
||||||
|
"usbErrorPermissionDenied": "L'accès via USB a été refusé.",
|
||||||
|
"usbErrorDeviceMissing": "Le périphérique USB sélectionné n'est plus disponible.",
|
||||||
|
"usbErrorInvalidPort": "Sélectionnez un périphérique USB valide.",
|
||||||
|
"usbErrorBusy": "Une autre demande de connexion USB est déjà en cours.",
|
||||||
|
"usbErrorNotConnected": "Aucun appareil USB n'est connecté.",
|
||||||
|
"usbErrorOpenFailed": "Impossible d'ouvrir l'appareil USB sélectionné.",
|
||||||
|
"usbErrorConnectFailed": "Impossible de se connecter à l'appareil USB sélectionné.",
|
||||||
|
"usbErrorUnsupported": "La communication série USB n'est pas prise en charge sur cette plateforme.",
|
||||||
|
"usbErrorAlreadyActive": "Une connexion USB est déjà établie.",
|
||||||
|
"usbErrorNoDeviceSelected": "Aucun appareil USB n'a été sélectionné.",
|
||||||
|
"usbErrorPortClosed": "La connexion USB n'est pas établie.",
|
||||||
|
"usbFallbackDeviceName": "Dispositif de communication série sur le Web",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_notConnected": "Sélectionnez un périphérique USB",
|
||||||
|
"usbConnectionFailed": "Échec de la connexion USB : {error}",
|
||||||
|
"usbStatus_connecting": "Connexion au périphérique USB...",
|
||||||
|
"usbStatus_searching": "Recherche de périphériques USB...",
|
||||||
|
"usbErrorConnectTimedOut": "La connexion a expiré. Assurez-vous que l'appareil dispose du firmware USB Companion.",
|
||||||
|
"@tcpStatus_connectingTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"endpoint": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@tcpConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tcpHostLabel": "Adresse IP",
|
||||||
|
"connectionChoiceTcpLabel": "TCP",
|
||||||
|
"tcpScreenTitle": "Établir une connexion via TCP",
|
||||||
|
"tcpHostHint": "192.168.40.10",
|
||||||
|
"tcpPortLabel": "Port",
|
||||||
|
"tcpPortHint": "5000",
|
||||||
|
"tcpStatus_notConnected": "Entrez l'adresse de destination et connectez-vous.",
|
||||||
|
"tcpStatus_connectingTo": "Connexion à {endpoint}...",
|
||||||
|
"tcpErrorHostRequired": "Une adresse IP est obligatoire.",
|
||||||
|
"tcpErrorPortInvalid": "La taille du port doit être comprise entre 1 et 65535.",
|
||||||
|
"tcpErrorUnsupported": "Le protocole TCP n'est pas pris en charge sur cette plateforme.",
|
||||||
|
"tcpErrorTimedOut": "La connexion TCP a expiré.",
|
||||||
|
"tcpConnectionFailed": "Échec de la connexion TCP : {error}",
|
||||||
|
"map_showDiscoveryContacts": "Afficher les contacts de découverte",
|
||||||
|
"map_setAsMyLocation": "Définir comme ma localisation",
|
||||||
|
"@path_routeWeight": {
|
||||||
|
"placeholders": {
|
||||||
|
"weight": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"max": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings_privacy": "Paramètres de confidentialité",
|
||||||
|
"settings_privacySubtitle": "Contrôlez les informations partagées",
|
||||||
|
"settings_telemetryLocationMode": "Mode d'emplacement de télémétrie",
|
||||||
|
"settings_telemetryEnvironmentMode": "Mode d'environnement de télémétrie",
|
||||||
|
"settings_advertLocation": "Emplacement de l'annonce",
|
||||||
|
"settings_advertLocationSubtitle": "Inclure l'emplacement dans l'annonce",
|
||||||
|
"settings_denyAll": "Refuser tout",
|
||||||
|
"settings_allowByContact": "Autoriser par drapeaux de contact",
|
||||||
|
"settings_privacySettingsDescription": "Choisissez les informations que votre appareil partage avec les autres.",
|
||||||
|
"settings_allowAll": "Autoriser tout",
|
||||||
|
"contact_info": "Informations de contact",
|
||||||
|
"settings_telemetryBaseMode": "Mode de base Télémétrie",
|
||||||
|
"contact_teleBase": "Base de télémétrie",
|
||||||
|
"contact_teleLoc": "Emplacement de télémétrie",
|
||||||
|
"contact_teleLocSubtitle": "Autoriser le partage des données de localisation",
|
||||||
|
"contact_teleEnv": "Environnement Télémétrie",
|
||||||
|
"contact_teleEnvSubtitle": "Autoriser le partage des données des capteurs d'environnement",
|
||||||
|
"contact_telemetry": "Télémétrie",
|
||||||
|
"contact_settings": "Paramètres de contact",
|
||||||
|
"contact_lastSeen": "Dernière fois vu",
|
||||||
|
"contact_clearChat": "Effacer la conversation",
|
||||||
|
"contact_teleBaseSubtitle": "Autoriser le partage du niveau de batterie et de la télémétrie de base",
|
||||||
|
"@settings_multiAck": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"appSettings_maxRouteWeightSubtitle": "Poids maximal qu'un itinéraire peut accumuler grâce à des livraisons réussies.",
|
||||||
|
"appSettings_initialRouteWeight": "Poids initial de l'itinéraire",
|
||||||
|
"appSettings_maxRouteWeight": "Poids maximal autorisé pour le trajet",
|
||||||
|
"appSettings_initialRouteWeightSubtitle": "Poids de départ pour les nouveaux chemins découverts",
|
||||||
|
"appSettings_routeWeightSuccessIncrement": "Augmentation du poids de réussite",
|
||||||
|
"appSettings_routeWeightSuccessIncrementSubtitle": "Poids ajouté à un itinéraire après une livraison réussie.",
|
||||||
|
"appSettings_routeWeightFailureDecrement": "Réduction du poids de pénalité",
|
||||||
|
"appSettings_routeWeightFailureDecrementSubtitle": "Poids retiré d'un itinéraire après une tentative de livraison infructueuse.",
|
||||||
|
"appSettings_maxMessageRetries": "Nombre maximal de tentatives de récupération de messages",
|
||||||
|
"appSettings_maxMessageRetriesSubtitle": "Nombre de tentatives de relance avant de marquer un message comme ayant échoué.",
|
||||||
|
"path_routeWeight": "{weight}/{max}",
|
||||||
|
"settings_multiAck": "Multi-ACKs : {value}",
|
||||||
|
"settings_telemetryModeUpdated": "Le mode télémétrie a été mis à jour",
|
||||||
|
"map_showOverlaps": "Chevauchement de la touche répétitive",
|
||||||
|
"map_runTraceWithReturnPath": "Revenir sur le même chemin."
|
||||||
}
|
}
|
||||||
+145
-1
@@ -285,6 +285,7 @@
|
|||||||
"contacts_newGroup": "Nuovo Gruppo",
|
"contacts_newGroup": "Nuovo Gruppo",
|
||||||
"contacts_groupName": "Nome gruppo",
|
"contacts_groupName": "Nome gruppo",
|
||||||
"contacts_groupNameRequired": "Il nome del gruppo è obbligatorio.",
|
"contacts_groupNameRequired": "Il nome del gruppo è obbligatorio.",
|
||||||
|
"contacts_groupNameReserved": "Questo nome del gruppo è riservato",
|
||||||
"contacts_groupAlreadyExists": "Il gruppo \"{name}\" esiste già.",
|
"contacts_groupAlreadyExists": "Il gruppo \"{name}\" esiste già.",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1605,6 +1606,8 @@
|
|||||||
"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_nearByRepeaters": "Ripetitori vicini",
|
||||||
"snrIndicator_lastSeen": "Ultimo accesso",
|
"snrIndicator_lastSeen": "Ultimo accesso",
|
||||||
@@ -1799,5 +1802,146 @@
|
|||||||
"contacts_searchFavorites": "Cerca {number}{str} Preferiti...",
|
"contacts_searchFavorites": "Cerca {number}{str} Preferiti...",
|
||||||
"contacts_unread": "Non letti",
|
"contacts_unread": "Non letti",
|
||||||
"contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...",
|
"contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...",
|
||||||
"contacts_searchRoomServers": "Cerca {number}{str} server Room..."
|
"contacts_searchRoomServers": "Cerca {number}{str} server Room...",
|
||||||
|
"contactsSettings_title": "Impostazioni dei contatti",
|
||||||
|
"settings_contactSettings": "Impostazioni di contatto",
|
||||||
|
"contactsSettings_otherTitle": "Altre impostazioni relative ai contatti",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Consenti al compagno di aggiungere automaticamente gli utenti scoperti.",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Aggiungere ripetitori automaticamente",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Consenti al compagno di aggiungere automaticamente i server delle stanze scoperte.",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Aggiungere automaticamente i sensori",
|
||||||
|
"settings_contactSettingsSubtitle": "Impostazioni per l'aggiunta dei contatti",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Aggiungere utenti automaticamente",
|
||||||
|
"contactsSettings_autoAddTitle": "Scoperta automatica",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Consenti al compagno di aggiungere automaticamente i sensori scoperti",
|
||||||
|
"discoveredContacts_noMatching": "Nessun contatto corrispondente",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Consenti al compagno di aggiungere automaticamente i ripetitori scoperti.",
|
||||||
|
"discoveredContacts_searchHint": "Cerca contatti scoperti",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Aggiungere automaticamente i server delle stanze",
|
||||||
|
"discoveredContacts_addContact": "Aggiungi contatto",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Sostituisci il più vecchio",
|
||||||
|
"discoveredContacts_Title": "Contatti scoperti",
|
||||||
|
"discoveredContacts_contactAdded": "Contatto aggiunto",
|
||||||
|
"discoveredContacts_deleteContact": "Elimina Contatto",
|
||||||
|
"discoveredContacts_copyContact": "Copia contatto negli appunti",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Quando l'elenco dei contatti è pieno, il contatto più vecchio non tra i preferiti verrà sostituito.",
|
||||||
|
"common_deleteAll": "Elimina tutto",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Sei sicuro di voler eliminare tutti i contatti scoperti?",
|
||||||
|
"discoveredContacts_deleteContactAll": "Eliminare tutti i contatti scoperti",
|
||||||
|
"map_guessedLocation": "Località indovinata",
|
||||||
|
"map_showGuessedLocations": "Mostra le posizioni stimate dei nodi",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenSubtitle": "Seleziona il dispositivo seriale rilevato e connettilo direttamente al tuo nodo MeshCore.",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenTitle": "Connessione tramite USB",
|
||||||
|
"usbScreenStatus": "Seleziona un dispositivo USB",
|
||||||
|
"usbScreenNote": "La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.",
|
||||||
|
"usbScreenEmptyState": "Nessun dispositivo USB rilevato. Collegare uno e aggiornare.",
|
||||||
|
"usbErrorPermissionDenied": "È stato negato l'accesso tramite USB.",
|
||||||
|
"usbErrorDeviceMissing": "Il dispositivo USB selezionato non è più disponibile.",
|
||||||
|
"usbErrorInvalidPort": "Seleziona un dispositivo USB valido.",
|
||||||
|
"usbErrorBusy": "Un'altra richiesta di connessione tramite USB è già in corso.",
|
||||||
|
"usbErrorNotConnected": "Non è collegato alcun dispositivo USB.",
|
||||||
|
"usbErrorOpenFailed": "Impossibile aprire il dispositivo USB selezionato.",
|
||||||
|
"usbErrorConnectFailed": "Impossibile connettersi al dispositivo USB selezionato.",
|
||||||
|
"usbErrorUnsupported": "La comunicazione seriale tramite USB non è supportata su questa piattaforma.",
|
||||||
|
"usbErrorAlreadyActive": "La connessione USB è già attiva.",
|
||||||
|
"usbErrorNoDeviceSelected": "Non è stato selezionato alcun dispositivo USB.",
|
||||||
|
"usbErrorPortClosed": "La connessione USB non è attiva.",
|
||||||
|
"usbFallbackDeviceName": "Dispositivo per comunicazione seriale su rete",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_searching": "Ricerca di dispositivi USB...",
|
||||||
|
"usbConnectionFailed": "Errore nella connessione USB: {error}",
|
||||||
|
"usbStatus_notConnected": "Seleziona un dispositivo USB",
|
||||||
|
"usbStatus_connecting": "Connessione al dispositivo USB...",
|
||||||
|
"usbErrorConnectTimedOut": "La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion.",
|
||||||
|
"@tcpStatus_connectingTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"endpoint": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@tcpConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tcpHostLabel": "Indirizzo IP",
|
||||||
|
"tcpHostHint": "192.168.40.10",
|
||||||
|
"connectionChoiceTcpLabel": "TCP",
|
||||||
|
"tcpScreenTitle": "Stabilire una connessione tramite TCP",
|
||||||
|
"tcpPortLabel": "Porta",
|
||||||
|
"tcpPortHint": "5000",
|
||||||
|
"tcpStatus_notConnected": "Inserisci l'endpoint e connettiti.",
|
||||||
|
"tcpStatus_connectingTo": "Connessione a {endpoint}...",
|
||||||
|
"tcpErrorHostRequired": "È necessario fornire un indirizzo IP.",
|
||||||
|
"tcpErrorPortInvalid": "La dimensione della porta deve essere compresa tra 1 e 65535.",
|
||||||
|
"tcpErrorUnsupported": "Il protocollo TCP non è supportato su questa piattaforma.",
|
||||||
|
"tcpErrorTimedOut": "La connessione TCP è scaduta.",
|
||||||
|
"tcpConnectionFailed": "Impossibile stabilire la connessione TCP: {error}",
|
||||||
|
"map_showDiscoveryContacts": "Mostra Contatti di Discovery",
|
||||||
|
"map_setAsMyLocation": "Imposta come la mia posizione",
|
||||||
|
"@path_routeWeight": {
|
||||||
|
"placeholders": {
|
||||||
|
"weight": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"max": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings_privacySettingsDescription": "Scegli le informazioni che il tuo dispositivo condivide con gli altri.",
|
||||||
|
"settings_allowByContact": "Consenti in base ai flag di contatto",
|
||||||
|
"settings_telemetryLocationMode": "Modalità di posizionamento telemetrico",
|
||||||
|
"settings_telemetryEnvironmentMode": "Modalità di ambiente di telemetria",
|
||||||
|
"settings_advertLocation": "Posizione dell'annuncio",
|
||||||
|
"settings_advertLocationSubtitle": "Includi la posizione nell'annuncio",
|
||||||
|
"settings_privacy": "Impostazioni sulla privacy",
|
||||||
|
"settings_denyAll": "Negare tutto",
|
||||||
|
"settings_privacySubtitle": "Controlla le informazioni che vengono condivise.",
|
||||||
|
"settings_allowAll": "Consenti tutto",
|
||||||
|
"contact_info": "Informazioni di Contatto",
|
||||||
|
"settings_telemetryBaseMode": "Modalità di base di telemetria",
|
||||||
|
"contact_teleBase": "Base di telemetria",
|
||||||
|
"contact_teleLoc": "Posizione telemetria",
|
||||||
|
"contact_teleLocSubtitle": "Consenti la condivisione dei dati di posizione",
|
||||||
|
"contact_clearChat": "Cancella chat",
|
||||||
|
"contact_telemetry": "Telemetria",
|
||||||
|
"contact_settings": "Impostazioni di contatto",
|
||||||
|
"contact_lastSeen": "Ultimo accesso",
|
||||||
|
"contact_teleBaseSubtitle": "Consenti la condivisione del livello della batteria e della telemetria di base",
|
||||||
|
"contact_teleEnvSubtitle": "Consenti la condivisione dei dati del sensore ambientale",
|
||||||
|
"contact_teleEnv": "Ambiente di telemetria",
|
||||||
|
"@settings_multiAck": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"appSettings_initialRouteWeight": "Peso iniziale del percorso",
|
||||||
|
"appSettings_initialRouteWeightSubtitle": "Peso di partenza per nuovi percorsi",
|
||||||
|
"appSettings_maxRouteWeightSubtitle": "Il peso massimo che un percorso può accumulare grazie a consegne di successo.",
|
||||||
|
"appSettings_maxRouteWeight": "Massimo peso consentito per il percorso",
|
||||||
|
"appSettings_routeWeightSuccessIncrement": "Aumento del peso del successo",
|
||||||
|
"appSettings_routeWeightSuccessIncrementSubtitle": "Peso aggiunto a un percorso dopo una consegna riuscita.",
|
||||||
|
"appSettings_routeWeightFailureDecrement": "Riduzione del peso associato al fallimento",
|
||||||
|
"appSettings_routeWeightFailureDecrementSubtitle": "Peso rimosso da un percorso dopo un tentativo di consegna fallito.",
|
||||||
|
"appSettings_maxMessageRetries": "Numero massimo di tentativi di invio del messaggio",
|
||||||
|
"appSettings_maxMessageRetriesSubtitle": "Numero di tentativi di riprova prima di considerare un messaggio come fallito.",
|
||||||
|
"path_routeWeight": "{weight}/{max}",
|
||||||
|
"settings_telemetryModeUpdated": "Modalità telemetria aggiornata",
|
||||||
|
"settings_multiAck": "Multi-ACKs: {value}",
|
||||||
|
"map_showOverlaps": "Sovrapposizioni della chiave ripetitore",
|
||||||
|
"map_runTraceWithReturnPath": "Tornare indietro sullo stesso percorso"
|
||||||
}
|
}
|
||||||
@@ -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,228 @@ 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 @connectionChoiceTcpLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'TCP'**
|
||||||
|
String get connectionChoiceTcpLabel;
|
||||||
|
|
||||||
|
/// No description provided for @tcpScreenTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Connect over TCP'**
|
||||||
|
String get tcpScreenTitle;
|
||||||
|
|
||||||
|
/// No description provided for @tcpHostLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'IP Address'**
|
||||||
|
String get tcpHostLabel;
|
||||||
|
|
||||||
|
/// No description provided for @tcpHostHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'192.168.40.10'**
|
||||||
|
String get tcpHostHint;
|
||||||
|
|
||||||
|
/// No description provided for @tcpPortLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Port'**
|
||||||
|
String get tcpPortLabel;
|
||||||
|
|
||||||
|
/// No description provided for @tcpPortHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'5000'**
|
||||||
|
String get tcpPortHint;
|
||||||
|
|
||||||
|
/// No description provided for @tcpStatus_notConnected.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Enter endpoint and connect'**
|
||||||
|
String get tcpStatus_notConnected;
|
||||||
|
|
||||||
|
/// No description provided for @tcpStatus_connectingTo.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Connecting to {endpoint}...'**
|
||||||
|
String tcpStatus_connectingTo(String endpoint);
|
||||||
|
|
||||||
|
/// No description provided for @tcpErrorHostRequired.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'IP address is required.'**
|
||||||
|
String get tcpErrorHostRequired;
|
||||||
|
|
||||||
|
/// No description provided for @tcpErrorPortInvalid.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Port must be between 1 and 65535.'**
|
||||||
|
String get tcpErrorPortInvalid;
|
||||||
|
|
||||||
|
/// No description provided for @tcpErrorUnsupported.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'TCP transport is not supported on this platform.'**
|
||||||
|
String get tcpErrorUnsupported;
|
||||||
|
|
||||||
|
/// No description provided for @tcpErrorTimedOut.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'TCP connection timed out.'**
|
||||||
|
String get tcpErrorTimedOut;
|
||||||
|
|
||||||
|
/// No description provided for @tcpConnectionFailed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'TCP connection failed: {error}'**
|
||||||
|
String tcpConnectionFailed(String error);
|
||||||
|
|
||||||
|
/// No description provided for @usbScreenTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Connect over USB'**
|
||||||
|
String get usbScreenTitle;
|
||||||
|
|
||||||
|
/// No description provided for @usbScreenSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Choose a detected serial device and connect directly to your MeshCore node.'**
|
||||||
|
String get usbScreenSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @usbScreenStatus.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Select a USB device'**
|
||||||
|
String get usbScreenStatus;
|
||||||
|
|
||||||
|
/// No description provided for @usbScreenNote.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'USB serial is active on supported Android devices and desktop platforms.'**
|
||||||
|
String get usbScreenNote;
|
||||||
|
|
||||||
|
/// No description provided for @usbScreenEmptyState.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No USB devices found. Plug one in and refresh.'**
|
||||||
|
String get usbScreenEmptyState;
|
||||||
|
|
||||||
|
/// No description provided for @usbErrorPermissionDenied.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'USB permission was denied.'**
|
||||||
|
String get usbErrorPermissionDenied;
|
||||||
|
|
||||||
|
/// No description provided for @usbErrorDeviceMissing.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'The selected USB device is no longer available.'**
|
||||||
|
String get usbErrorDeviceMissing;
|
||||||
|
|
||||||
|
/// No description provided for @usbErrorInvalidPort.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Select a valid USB device.'**
|
||||||
|
String get usbErrorInvalidPort;
|
||||||
|
|
||||||
|
/// No description provided for @usbErrorBusy.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Another USB connection request is already in progress.'**
|
||||||
|
String get usbErrorBusy;
|
||||||
|
|
||||||
|
/// No description provided for @usbErrorNotConnected.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No USB device is connected.'**
|
||||||
|
String get usbErrorNotConnected;
|
||||||
|
|
||||||
|
/// No description provided for @usbErrorOpenFailed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Failed to open the selected USB device.'**
|
||||||
|
String get usbErrorOpenFailed;
|
||||||
|
|
||||||
|
/// No description provided for @usbErrorConnectFailed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Failed to connect to the selected USB device.'**
|
||||||
|
String get usbErrorConnectFailed;
|
||||||
|
|
||||||
|
/// No description provided for @usbErrorUnsupported.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'USB serial is not supported on this platform.'**
|
||||||
|
String get usbErrorUnsupported;
|
||||||
|
|
||||||
|
/// No description provided for @usbErrorAlreadyActive.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'A USB connection is already active.'**
|
||||||
|
String get usbErrorAlreadyActive;
|
||||||
|
|
||||||
|
/// No description provided for @usbErrorNoDeviceSelected.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No USB device was selected.'**
|
||||||
|
String get usbErrorNoDeviceSelected;
|
||||||
|
|
||||||
|
/// No description provided for @usbErrorPortClosed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'The USB connection is not open.'**
|
||||||
|
String get usbErrorPortClosed;
|
||||||
|
|
||||||
|
/// No description provided for @usbErrorConnectTimedOut.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Connection timed out. Make sure the device has USB Companion firmware.'**
|
||||||
|
String get usbErrorConnectTimedOut;
|
||||||
|
|
||||||
|
/// No description provided for @usbFallbackDeviceName.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Web Serial Device'**
|
||||||
|
String get usbFallbackDeviceName;
|
||||||
|
|
||||||
|
/// No description provided for @usbStatus_notConnected.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Select a USB device'**
|
||||||
|
String get usbStatus_notConnected;
|
||||||
|
|
||||||
|
/// No description provided for @usbStatus_connecting.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Connecting to USB device...'**
|
||||||
|
String get usbStatus_connecting;
|
||||||
|
|
||||||
|
/// No description provided for @usbStatus_searching.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Searching for USB devices...'**
|
||||||
|
String get usbStatus_searching;
|
||||||
|
|
||||||
|
/// No description provided for @usbConnectionFailed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'USB connection failed: {error}'**
|
||||||
|
String usbConnectionFailed(String error);
|
||||||
|
|
||||||
/// No description provided for @scanner_scanning.
|
/// No description provided for @scanner_scanning.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -388,6 +616,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 +784,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:
|
||||||
@@ -574,6 +826,84 @@ abstract class AppLocalizations {
|
|||||||
/// **'Privacy mode disabled'**
|
/// **'Privacy mode disabled'**
|
||||||
String get settings_privacyModeDisabled;
|
String get settings_privacyModeDisabled;
|
||||||
|
|
||||||
|
/// No description provided for @settings_privacy.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Privacy Settings'**
|
||||||
|
String get settings_privacy;
|
||||||
|
|
||||||
|
/// No description provided for @settings_privacySubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Control what information is shared.'**
|
||||||
|
String get settings_privacySubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @settings_privacySettingsDescription.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Choose what information your device shares with others.'**
|
||||||
|
String get settings_privacySettingsDescription;
|
||||||
|
|
||||||
|
/// No description provided for @settings_denyAll.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Deny all'**
|
||||||
|
String get settings_denyAll;
|
||||||
|
|
||||||
|
/// No description provided for @settings_allowByContact.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Allow by contact flags'**
|
||||||
|
String get settings_allowByContact;
|
||||||
|
|
||||||
|
/// No description provided for @settings_allowAll.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Allow all'**
|
||||||
|
String get settings_allowAll;
|
||||||
|
|
||||||
|
/// No description provided for @settings_telemetryBaseMode.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Telemetry Base Mode'**
|
||||||
|
String get settings_telemetryBaseMode;
|
||||||
|
|
||||||
|
/// No description provided for @settings_telemetryLocationMode.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Telemetry Location Mode'**
|
||||||
|
String get settings_telemetryLocationMode;
|
||||||
|
|
||||||
|
/// No description provided for @settings_telemetryEnvironmentMode.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Telemetry Environment Mode'**
|
||||||
|
String get settings_telemetryEnvironmentMode;
|
||||||
|
|
||||||
|
/// No description provided for @settings_advertLocation.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Advert Location'**
|
||||||
|
String get settings_advertLocation;
|
||||||
|
|
||||||
|
/// No description provided for @settings_advertLocationSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Include location in advert.'**
|
||||||
|
String get settings_advertLocationSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @settings_multiAck.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Multi-ACKs: {value}'**
|
||||||
|
String settings_multiAck(String value);
|
||||||
|
|
||||||
|
/// No description provided for @settings_telemetryModeUpdated.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Telemetry mode updated'**
|
||||||
|
String get settings_telemetryModeUpdated;
|
||||||
|
|
||||||
/// No description provided for @settings_actions.
|
/// No description provided for @settings_actions.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -1108,6 +1438,72 @@ abstract class AppLocalizations {
|
|||||||
/// **'Auto route rotation disabled'**
|
/// **'Auto route rotation disabled'**
|
||||||
String get appSettings_autoRouteRotationDisabled;
|
String get appSettings_autoRouteRotationDisabled;
|
||||||
|
|
||||||
|
/// No description provided for @appSettings_maxRouteWeight.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Max Route Weight'**
|
||||||
|
String get appSettings_maxRouteWeight;
|
||||||
|
|
||||||
|
/// No description provided for @appSettings_maxRouteWeightSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Maximum weight a path can accumulate from successful deliveries'**
|
||||||
|
String get appSettings_maxRouteWeightSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @appSettings_initialRouteWeight.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Initial Route Weight'**
|
||||||
|
String get appSettings_initialRouteWeight;
|
||||||
|
|
||||||
|
/// No description provided for @appSettings_initialRouteWeightSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Starting weight for newly discovered paths'**
|
||||||
|
String get appSettings_initialRouteWeightSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @appSettings_routeWeightSuccessIncrement.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Success Weight Increment'**
|
||||||
|
String get appSettings_routeWeightSuccessIncrement;
|
||||||
|
|
||||||
|
/// No description provided for @appSettings_routeWeightSuccessIncrementSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Weight added to a path after successful delivery'**
|
||||||
|
String get appSettings_routeWeightSuccessIncrementSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @appSettings_routeWeightFailureDecrement.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Failure Weight Decrement'**
|
||||||
|
String get appSettings_routeWeightFailureDecrement;
|
||||||
|
|
||||||
|
/// No description provided for @appSettings_routeWeightFailureDecrementSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Weight removed from a path after failed delivery'**
|
||||||
|
String get appSettings_routeWeightFailureDecrementSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @appSettings_maxMessageRetries.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Max Message Retries'**
|
||||||
|
String get appSettings_maxMessageRetries;
|
||||||
|
|
||||||
|
/// No description provided for @appSettings_maxMessageRetriesSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Number of retry attempts before marking a message as failed'**
|
||||||
|
String get appSettings_maxMessageRetriesSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @path_routeWeight.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{weight}/{max}'**
|
||||||
|
String path_routeWeight(String weight, String max);
|
||||||
|
|
||||||
/// No description provided for @appSettings_battery.
|
/// No description provided for @appSettings_battery.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -1462,6 +1858,12 @@ abstract class AppLocalizations {
|
|||||||
/// **'Group name is required'**
|
/// **'Group name is required'**
|
||||||
String get contacts_groupNameRequired;
|
String get contacts_groupNameRequired;
|
||||||
|
|
||||||
|
/// No description provided for @contacts_groupNameReserved.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'This group name is reserved'**
|
||||||
|
String get contacts_groupNameReserved;
|
||||||
|
|
||||||
/// No description provided for @contacts_groupAlreadyExists.
|
/// No description provided for @contacts_groupAlreadyExists.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -1489,39 +1891,105 @@ 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 @contact_info.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Contact Info'**
|
||||||
|
String get contact_info;
|
||||||
|
|
||||||
|
/// No description provided for @contact_settings.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Contact Settings'**
|
||||||
|
String get contact_settings;
|
||||||
|
|
||||||
|
/// No description provided for @contact_telemetry.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Telemetry'**
|
||||||
|
String get contact_telemetry;
|
||||||
|
|
||||||
|
/// No description provided for @contact_lastSeen.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Last seen'**
|
||||||
|
String get contact_lastSeen;
|
||||||
|
|
||||||
|
/// No description provided for @contact_clearChat.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Clear Chat'**
|
||||||
|
String get contact_clearChat;
|
||||||
|
|
||||||
|
/// No description provided for @contact_teleBase.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Telemetry Base'**
|
||||||
|
String get contact_teleBase;
|
||||||
|
|
||||||
|
/// No description provided for @contact_teleBaseSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Allow sharing battery level and basic telemetry'**
|
||||||
|
String get contact_teleBaseSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @contact_teleLoc.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Telemetry Location'**
|
||||||
|
String get contact_teleLoc;
|
||||||
|
|
||||||
|
/// No description provided for @contact_teleLocSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Allow sharing location data'**
|
||||||
|
String get contact_teleLocSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @contact_teleEnv.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Telemetry Environment'**
|
||||||
|
String get contact_teleEnv;
|
||||||
|
|
||||||
|
/// No description provided for @contact_teleEnvSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Allow sharing environment sensor data'**
|
||||||
|
String get contact_teleEnvSubtitle;
|
||||||
|
|
||||||
/// No description provided for @channels_title.
|
/// No description provided for @channels_title.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -2494,6 +2962,12 @@ abstract class AppLocalizations {
|
|||||||
/// **'Share marker here'**
|
/// **'Share marker here'**
|
||||||
String get map_shareMarkerHere;
|
String get map_shareMarkerHere;
|
||||||
|
|
||||||
|
/// No description provided for @map_setAsMyLocation.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Set as my location'**
|
||||||
|
String get map_setAsMyLocation;
|
||||||
|
|
||||||
/// No description provided for @map_pinLabel.
|
/// No description provided for @map_pinLabel.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -2578,6 +3052,12 @@ abstract class AppLocalizations {
|
|||||||
/// **'Other Nodes'**
|
/// **'Other Nodes'**
|
||||||
String get map_otherNodes;
|
String get map_otherNodes;
|
||||||
|
|
||||||
|
/// No description provided for @map_showOverlaps.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Repeater Key Overlaps'**
|
||||||
|
String get map_showOverlaps;
|
||||||
|
|
||||||
/// No description provided for @map_keyPrefix.
|
/// No description provided for @map_keyPrefix.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -2608,6 +3088,24 @@ 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_showDiscoveryContacts.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Show Discovery Contacts'**
|
||||||
|
String get map_showDiscoveryContacts;
|
||||||
|
|
||||||
|
/// No description provided for @map_guessedLocation.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Guessed location'**
|
||||||
|
String get map_guessedLocation;
|
||||||
|
|
||||||
/// No description provided for @map_lastSeenTime.
|
/// No description provided for @map_lastSeenTime.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -2641,9 +3139,15 @@ abstract class AppLocalizations {
|
|||||||
/// No description provided for @map_runTrace.
|
/// No description provided for @map_runTrace.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Run Path Trace'**
|
/// **'Run path trace'**
|
||||||
String get map_runTrace;
|
String get map_runTrace;
|
||||||
|
|
||||||
|
/// No description provided for @map_runTraceWithReturnPath.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Return back on the same path.'**
|
||||||
|
String get map_runTraceWithReturnPath;
|
||||||
|
|
||||||
/// No description provided for @map_removeLast.
|
/// No description provided for @map_removeLast.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -5380,6 +5884,138 @@ abstract class AppLocalizations {
|
|||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Last seen'**
|
/// **'Last seen'**
|
||||||
String get snrIndicator_lastSeen;
|
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,134 @@ 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 connectionChoiceTcpLabel => 'TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpScreenTitle => 'Свържете се чрез TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostLabel => 'IP адрес';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostHint => '192.168.40.10';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortLabel => 'Пристанище';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortHint => '5000';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpStatus_notConnected => 'Въведете крайната точка и свържете се.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpStatus_connectingTo(String endpoint) {
|
||||||
|
return 'Свързване към $endpoint...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorHostRequired => 'Необходим е IP адрес.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorPortInvalid => 'Портът трябва да бъде между 1 и 65535.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorUnsupported =>
|
||||||
|
'Транспортът чрез TCP не се поддържа на тази платформа.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorTimedOut => 'Връзката TCP изтекла.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpConnectionFailed(String error) {
|
||||||
|
return 'Неуспешно е установено TCP връзката: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Свържете се чрез USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Изберете открития сериен уред и свържете директно към вашия MeshCore възел.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Изберете USB устройство';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'Няма открити USB устройства. Включете едно и опитайте отново.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied => 'Не беше разрешено достъпът през USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing =>
|
||||||
|
'Избраното USB устройство вече не е налично.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Изберете валитно USB устройство.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy =>
|
||||||
|
'Друг мол за свързване през USB вече е в процес на изпълнение.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'Няма свързано USB устройство.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed =>
|
||||||
|
'Не успях да отворя избраното USB устройство.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'Не успях да се свържа с избраното USB устройство.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'USB серийната комуникация не се поддържа на тази платформа.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'USB връзката вече е активирана.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected => 'Няма избран USB устройство.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'USB връзката не е активна.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName =>
|
||||||
|
'Устройство за четене на уеб серийни данни';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Изберете USB устройство';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Свързване към USB устройство...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Търсене на USB устройства...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'Неуспешно свързване през USB: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Сканиране за устройства...';
|
String get scanner_scanning => 'Сканиране за устройства...';
|
||||||
|
|
||||||
@@ -150,6 +281,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 +372,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 => 'Режим на поверителност';
|
||||||
|
|
||||||
@@ -253,6 +398,52 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
String get settings_privacyModeDisabled =>
|
String get settings_privacyModeDisabled =>
|
||||||
'Режим на поверителност е деактивиран';
|
'Режим на поверителност е деактивиран';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacy => 'Настройки на поверителността';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySubtitle =>
|
||||||
|
'Контролирайте каква информация се споделя.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySettingsDescription =>
|
||||||
|
'Изберете каква информация устройството ви споделя с другите.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_denyAll => 'Откажи всичко';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowByContact => 'Позволи по флагове за контакт';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowAll => 'Позволи всичко';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryBaseMode => 'Базов режим на телеметрия';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryLocationMode =>
|
||||||
|
'Режим на местоположение на телеметрията';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryEnvironmentMode =>
|
||||||
|
'Режим на средата на телеметрията';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocation => 'Място на обявата';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocationSubtitle =>
|
||||||
|
'Включи местоположение в обявата';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_multiAck(String value) {
|
||||||
|
return 'Мулти-потвърди: $value';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryModeUpdated => 'Режим на телеметрията е обновен';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_actions => 'Действия';
|
String get settings_actions => 'Действия';
|
||||||
|
|
||||||
@@ -550,6 +741,51 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
String get appSettings_autoRouteRotationDisabled =>
|
String get appSettings_autoRouteRotationDisabled =>
|
||||||
'Автоматично маршрутизирането е деактивирано';
|
'Автоматично маршрутизирането е деактивирано';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeight =>
|
||||||
|
'Максимално допустимо тегло на маршрута';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeightSubtitle =>
|
||||||
|
'Максималното тегло, което един маршрут може да събере от успешни доставки.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeight =>
|
||||||
|
'Първоначална тежест на маршрута';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeightSubtitle =>
|
||||||
|
'Начално тегло за новооткрити маршрути';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrement =>
|
||||||
|
'Увеличение на теглото за успех';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||||
|
'Тегло, добавено към път след успешно доставяне.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrement =>
|
||||||
|
'Намаляване на теглото, свързано с неуспех';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||||
|
'Тегло, което е било премахнато от пътя след неуспешен опит за доставка.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetries =>
|
||||||
|
'Максимален брой опити за изпращане на съобщение';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetriesSubtitle =>
|
||||||
|
'Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String path_routeWeight(String weight, String max) {
|
||||||
|
return '$weight/$max';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_battery => 'Батерия';
|
String get appSettings_battery => 'Батерия';
|
||||||
|
|
||||||
@@ -757,6 +993,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'Името на групата е задължително.';
|
String get contacts_groupNameRequired => 'Името на групата е задължително.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => 'Това име на група е запазено';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'Групата \"$name\" вече съществува.';
|
return 'Групата \"$name\" вече съществува.';
|
||||||
@@ -796,6 +1035,42 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
return 'Последно видян $days дни преди.';
|
return 'Последно видян $days дни преди.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_info => 'Контактна информация';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_settings => 'Настройки за контакти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_telemetry => 'Телеметрия';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_lastSeen => 'Последно видян';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_clearChat => 'Изчисти чата';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBase => 'Базата данни за телеметрия';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBaseSubtitle =>
|
||||||
|
'Позволи споделяне на ниво на батерията и основна телеметрия';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLoc => 'Местоположение на телеметрията';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLocSubtitle =>
|
||||||
|
'Позволи споделяне на данни за местоположение';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnv => 'Среда на телеметрия';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnvSubtitle =>
|
||||||
|
'Позволи споделяне на данни от средносферните датчици';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_title => 'Канали';
|
String get channels_title => 'Канали';
|
||||||
|
|
||||||
@@ -1366,6 +1641,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Споделете маркер тук';
|
String get map_shareMarkerHere => 'Споделете маркер тук';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_setAsMyLocation => 'Задайте като моя местоположение';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Етикетиране на пин';
|
String get map_pinLabel => 'Етикетиране на пин';
|
||||||
|
|
||||||
@@ -1411,6 +1689,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_otherNodes => 'Други възли';
|
String get map_otherNodes => 'Други възли';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showOverlaps => 'Покриване на ключа на повтаряча';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_keyPrefix => 'Префикс на ключа';
|
String get map_keyPrefix => 'Префикс на ключа';
|
||||||
|
|
||||||
@@ -1426,6 +1707,16 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_showSharedMarkers => 'Покажи споделени маркери';
|
String get map_showSharedMarkers => 'Покажи споделени маркери';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showGuessedLocations =>
|
||||||
|
'Покажете местоположенията на предположените възли.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showDiscoveryContacts => 'Покажи контакти за откриване';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_guessedLocation => 'Предполагано местоположение';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_lastSeenTime => 'Последна видяна дата';
|
String get map_lastSeenTime => 'Последна видяна дата';
|
||||||
|
|
||||||
@@ -1445,6 +1736,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_runTrace => 'Изпълни Път на Следване';
|
String get map_runTrace => 'Изпълни Път на Следване';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTraceWithReturnPath => 'Върни се по същия път.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_removeLast => 'Премахни Последно';
|
String get map_removeLast => 'Премахни Последно';
|
||||||
|
|
||||||
@@ -3112,4 +3406,82 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get snrIndicator_lastSeen => 'Последно видян';
|
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,137 @@ 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 connectionChoiceTcpLabel => 'TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpScreenTitle => 'Verbinden über TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostLabel => 'IP-Adresse';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostHint => '192.168.40.10';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortLabel => 'Port';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortHint => '5000';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpStatus_notConnected =>
|
||||||
|
'Geben Sie den Endpunkt ein und verbinden Sie sich.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpStatus_connectingTo(String endpoint) {
|
||||||
|
return 'Verbindung zu $endpoint...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorHostRequired => 'Eine IP-Adresse ist erforderlich.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorPortInvalid =>
|
||||||
|
'Die Portnummer muss zwischen 1 und 65535 liegen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorUnsupported =>
|
||||||
|
'Die TCP-Übertragung wird auf dieser Plattform nicht unterstützt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorTimedOut => 'Die TCP-Verbindung ist abgelaufen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpConnectionFailed(String error) {
|
||||||
|
return 'Fehler beim TCP-Verbindungsaufbau: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Verbinden über USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Wählen Sie ein USB-Gerät aus';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'Die USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied =>
|
||||||
|
'Die USB-Berechtigung wurde abgelehnt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing =>
|
||||||
|
'Das ausgewählte USB-Gerät ist nicht mehr verfügbar.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Wählen Sie ein gültiges USB-Gerät aus.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy =>
|
||||||
|
'Eine weitere Anfrage für eine USB-Verbindung ist bereits in Bearbeitung.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'Es ist kein USB-Gerät angeschlossen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed =>
|
||||||
|
'Fehlgeschlagen beim Öffnen des ausgewählten USB-Geräts.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'Keine Verbindung zum ausgewählten USB-Gerät hergestellt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'Die USB-Serielle Schnittstelle wird auf dieser Plattform nicht unterstützt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive =>
|
||||||
|
'Eine USB-Verbindung ist bereits hergestellt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected => 'Kein USB-Gerät wurde ausgewählt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'Die USB-Verbindung ist nicht aktiv.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName => 'Web-Serielle Geräte';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Wählen Sie ein USB-Gerät aus';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Verbindung zum USB-Gerät...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Suche nach USB-Geräten...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'Fehler beim USB-Verbindungsaufbau: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Scannen nach Geräten...';
|
String get scanner_scanning => 'Scannen nach Geräten...';
|
||||||
|
|
||||||
@@ -150,6 +284,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 +374,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';
|
||||||
|
|
||||||
@@ -250,6 +398,50 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_privacyModeDisabled => 'Datenschutzmodus deaktiviert';
|
String get settings_privacyModeDisabled => 'Datenschutzmodus deaktiviert';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacy => 'Datenschutzeinstellungen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySubtitle =>
|
||||||
|
'Steuern Sie die Informationen, die freigegeben werden.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySettingsDescription =>
|
||||||
|
'Wählen Sie die Informationen, die Ihr Gerät mit anderen teilt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_denyAll => 'Alle ablehnen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowByContact => 'Zulassen durch Kontaktflaggen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowAll => 'Alles zulassen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryBaseMode => 'Telemetrie-Basismodus';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryLocationMode => 'Telemetrie-Ortsmodus';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryEnvironmentMode => 'Telemetrie-Umgebungsmodus';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocation => 'Anzeigenort';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocationSubtitle =>
|
||||||
|
'Ort in der Anzeige einbeziehen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_multiAck(String value) {
|
||||||
|
return 'Mehrfach-Bestätigungen: $value';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryModeUpdated => 'Telemetriemodus aktualisiert';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_actions => 'Aktionen';
|
String get settings_actions => 'Aktionen';
|
||||||
|
|
||||||
@@ -547,6 +739,49 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get appSettings_autoRouteRotationDisabled =>
|
String get appSettings_autoRouteRotationDisabled =>
|
||||||
'Automatische Routenrotation deaktiviert';
|
'Automatische Routenrotation deaktiviert';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeight => 'Maximale Gesamtstreckenlänge';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeightSubtitle =>
|
||||||
|
'Maximales Gewicht, das ein Weg durch erfolgreiche Lieferungen erreichen kann.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeight => 'Anfangs-Streckengewicht';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeightSubtitle =>
|
||||||
|
'Ausgangsgewicht für neu entdeckte Pfade';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrement =>
|
||||||
|
'Erhöhung des Erfolgsgewichts';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||||
|
'Gewicht, das einem Pfad nach erfolgreicher Lieferung hinzugefügt wird.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrement =>
|
||||||
|
'Reduzierung des Gewichts bei Fehlern';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||||
|
'Gewicht, das nach einem fehlgeschlagenen Versand von einem Weg entfernt wurde';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetries =>
|
||||||
|
'Maximale Anzahl an Wiederholungsversuchen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetriesSubtitle =>
|
||||||
|
'Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String path_routeWeight(String weight, String max) {
|
||||||
|
return '$weight/$max';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_battery => 'Akku';
|
String get appSettings_battery => 'Akku';
|
||||||
|
|
||||||
@@ -754,6 +989,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'Der Gruppennamen ist erforderlich.';
|
String get contacts_groupNameRequired => 'Der Gruppennamen ist erforderlich.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => 'Dieser Gruppenname ist reserviert';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'Die Gruppe \"$name\" existiert bereits.';
|
return 'Die Gruppe \"$name\" existiert bereits.';
|
||||||
@@ -770,29 +1008,64 @@ 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
|
||||||
|
String get contact_info => 'Kontaktinformationen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_settings => 'Kontakteinstellungen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_telemetry => 'Telemetrie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_lastSeen => 'Zuletzt gesehen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_clearChat => 'Chat löschen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBase => 'Telemetriebasis';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBaseSubtitle =>
|
||||||
|
'Erlauben des Freigebens des Batteriestands und der grundlegenden Telemetrie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLoc => 'Telemetrieort';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLocSubtitle => 'Teilen von Standortdaten zulassen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnv => 'Telemetrieumgebung';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnvSubtitle =>
|
||||||
|
'Teilen von Umgebungsensordaten zulassen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_title => 'Kanäle';
|
String get channels_title => 'Kanäle';
|
||||||
|
|
||||||
@@ -1365,6 +1638,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Teilen Sie den Marker hier.';
|
String get map_shareMarkerHere => 'Teilen Sie den Marker hier.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_setAsMyLocation => 'Als meine aktuelle Position festlegen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Pin Name';
|
String get map_pinLabel => 'Pin Name';
|
||||||
|
|
||||||
@@ -1410,6 +1686,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_otherNodes => 'Andere Knoten';
|
String get map_otherNodes => 'Andere Knoten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showOverlaps => 'Überlappungen der Repeater-Taste';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_keyPrefix => 'Schlüsselpräfix';
|
String get map_keyPrefix => 'Schlüsselpräfix';
|
||||||
|
|
||||||
@@ -1425,6 +1704,16 @@ 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_showDiscoveryContacts => 'Entdeckungs-Kontakte anzeigen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_guessedLocation => 'Geschätzter Ort';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_lastSeenTime => 'Letzte Sichtung';
|
String get map_lastSeenTime => 'Letzte Sichtung';
|
||||||
|
|
||||||
@@ -1444,6 +1733,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_runTrace => 'Pfadverlauf ausführen';
|
String get map_runTrace => 'Pfadverlauf ausführen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTraceWithReturnPath =>
|
||||||
|
'Auf dem gleichen Pfad zurückkehren.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_removeLast => 'Letztes Entfernen';
|
String get map_removeLast => 'Letztes Entfernen';
|
||||||
|
|
||||||
@@ -3121,4 +3414,84 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get snrIndicator_lastSeen => 'Zuletzt gesehen';
|
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,132 @@ 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 connectionChoiceTcpLabel => 'TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpScreenTitle => 'Connect over TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostLabel => 'IP Address';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostHint => '192.168.40.10';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortLabel => 'Port';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortHint => '5000';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpStatus_notConnected => 'Enter endpoint and connect';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpStatus_connectingTo(String endpoint) {
|
||||||
|
return 'Connecting to $endpoint...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorHostRequired => 'IP address is required.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorPortInvalid => 'Port must be between 1 and 65535.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorUnsupported =>
|
||||||
|
'TCP transport is not supported on this platform.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorTimedOut => 'TCP connection timed out.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpConnectionFailed(String error) {
|
||||||
|
return 'TCP connection failed: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Connect over USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Choose a detected serial device and connect directly to your MeshCore node.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Select a USB device';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'USB serial is active on supported Android devices and desktop platforms.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'No USB devices found. Plug one in and refresh.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied => 'USB permission was denied.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing =>
|
||||||
|
'The selected USB device is no longer available.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Select a valid USB device.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy =>
|
||||||
|
'Another USB connection request is already in progress.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'No USB device is connected.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed => 'Failed to open the selected USB device.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'Failed to connect to the selected USB device.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'USB serial is not supported on this platform.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'A USB connection is already active.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected => 'No USB device was selected.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'The USB connection is not open.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'Connection timed out. Make sure the device has USB Companion firmware.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName => 'Web Serial Device';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Select a USB device';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Connecting to USB device...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Searching for USB devices...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'USB connection failed: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Scanning for devices...';
|
String get scanner_scanning => 'Scanning for devices...';
|
||||||
|
|
||||||
@@ -149,6 +278,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 +368,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';
|
||||||
|
|
||||||
@@ -249,6 +392,48 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_privacyModeDisabled => 'Privacy mode disabled';
|
String get settings_privacyModeDisabled => 'Privacy mode disabled';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacy => 'Privacy Settings';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySubtitle => 'Control what information is shared.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySettingsDescription =>
|
||||||
|
'Choose what information your device shares with others.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_denyAll => 'Deny all';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowByContact => 'Allow by contact flags';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowAll => 'Allow all';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryBaseMode => 'Telemetry Base Mode';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryLocationMode => 'Telemetry Location Mode';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryEnvironmentMode => 'Telemetry Environment Mode';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocation => 'Advert Location';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocationSubtitle => 'Include location in advert.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_multiAck(String value) {
|
||||||
|
return 'Multi-ACKs: $value';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryModeUpdated => 'Telemetry mode updated';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_actions => 'Actions';
|
String get settings_actions => 'Actions';
|
||||||
|
|
||||||
@@ -541,6 +726,48 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
String get appSettings_autoRouteRotationDisabled =>
|
String get appSettings_autoRouteRotationDisabled =>
|
||||||
'Auto route rotation disabled';
|
'Auto route rotation disabled';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeight => 'Max Route Weight';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeightSubtitle =>
|
||||||
|
'Maximum weight a path can accumulate from successful deliveries';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeight => 'Initial Route Weight';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeightSubtitle =>
|
||||||
|
'Starting weight for newly discovered paths';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrement =>
|
||||||
|
'Success Weight Increment';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||||
|
'Weight added to a path after successful delivery';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrement =>
|
||||||
|
'Failure Weight Decrement';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||||
|
'Weight removed from a path after failed delivery';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetries => 'Max Message Retries';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetriesSubtitle =>
|
||||||
|
'Number of retry attempts before marking a message as failed';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String path_routeWeight(String weight, String max) {
|
||||||
|
return '$weight/$max';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_battery => 'Battery';
|
String get appSettings_battery => 'Battery';
|
||||||
|
|
||||||
@@ -746,6 +973,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'Group name is required';
|
String get contacts_groupNameRequired => 'Group name is required';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => 'This group name is reserved';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'Group \"$name\" already exists';
|
return 'Group \"$name\" already exists';
|
||||||
@@ -761,29 +991,63 @@ 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
|
||||||
|
String get contact_info => 'Contact Info';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_settings => 'Contact Settings';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_telemetry => 'Telemetry';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_lastSeen => 'Last seen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_clearChat => 'Clear Chat';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBase => 'Telemetry Base';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBaseSubtitle =>
|
||||||
|
'Allow sharing battery level and basic telemetry';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLoc => 'Telemetry Location';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLocSubtitle => 'Allow sharing location data';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnv => 'Telemetry Environment';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnvSubtitle => 'Allow sharing environment sensor data';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_title => 'Channels';
|
String get channels_title => 'Channels';
|
||||||
|
|
||||||
@@ -1344,6 +1608,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Share marker here';
|
String get map_shareMarkerHere => 'Share marker here';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_setAsMyLocation => 'Set as my location';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Pin label';
|
String get map_pinLabel => 'Pin label';
|
||||||
|
|
||||||
@@ -1389,6 +1656,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_otherNodes => 'Other Nodes';
|
String get map_otherNodes => 'Other Nodes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showOverlaps => 'Repeater Key Overlaps';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_keyPrefix => 'Key Prefix';
|
String get map_keyPrefix => 'Key Prefix';
|
||||||
|
|
||||||
@@ -1404,6 +1674,15 @@ 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_showDiscoveryContacts => 'Show Discovery Contacts';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_guessedLocation => 'Guessed location';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_lastSeenTime => 'Last Seen Time';
|
String get map_lastSeenTime => 'Last Seen Time';
|
||||||
|
|
||||||
@@ -1420,7 +1699,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
String get map_tapToAdd => 'Tap on nodes to add them to the path.';
|
String get map_tapToAdd => 'Tap on nodes to add them to the path.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_runTrace => 'Run Path Trace';
|
String get map_runTrace => 'Run path trace';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTraceWithReturnPath => 'Return back on the same path.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_removeLast => 'Remove Last';
|
String get map_removeLast => 'Remove Last';
|
||||||
@@ -3065,4 +3347,78 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get snrIndicator_lastSeen => 'Last seen';
|
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,135 @@ 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 connectionChoiceTcpLabel => 'TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpScreenTitle => 'Establecer conexión a través de TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostLabel => 'Dirección IP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostHint => '192.168.40.10';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortLabel => 'Puerto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortHint => '5000';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpStatus_notConnected => 'Ingrese la dirección final y conecte.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpStatus_connectingTo(String endpoint) {
|
||||||
|
return 'Conectándose a $endpoint...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorHostRequired => 'Se requiere la dirección IP.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorPortInvalid => 'El puerto debe estar entre 1 y 65535.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorUnsupported =>
|
||||||
|
'El protocolo de transporte TCP no está soportado en esta plataforma.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorTimedOut => 'La conexión TCP ha caducado.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpConnectionFailed(String error) {
|
||||||
|
return 'Error en la conexión TCP: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Conecte mediante USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Seleccione el dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Seleccione un dispositivo USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'No se encontraron dispositivos USB. Conecte uno y vuelva a intentar.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied =>
|
||||||
|
'Se denegó el permiso de acceso a través de USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing =>
|
||||||
|
'El dispositivo USB seleccionado ya no está disponible.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Seleccione un dispositivo USB válido.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy =>
|
||||||
|
'Ya se ha iniciado una solicitud de conexión USB adicional.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'No hay ningún dispositivo USB conectado.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed =>
|
||||||
|
'No se pudo abrir el dispositivo USB seleccionado.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'No se pudo conectar con el dispositivo USB seleccionado.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'La comunicación serial a través de USB no está soportada en esta plataforma.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'La conexión USB ya está activa.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected =>
|
||||||
|
'No se ha seleccionado ningún dispositivo USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'La conexión USB no está activa.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName => 'Dispositivo de serie web';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Seleccione un dispositivo USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Conectándose al dispositivo USB...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Buscando dispositivos USB...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'Error al conectar mediante USB: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Escaneando dispositivos...';
|
String get scanner_scanning => 'Escaneando dispositivos...';
|
||||||
|
|
||||||
@@ -150,6 +282,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 +372,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';
|
||||||
|
|
||||||
@@ -250,6 +396,51 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_privacyModeDisabled => 'Modo de privacidad desactivado';
|
String get settings_privacyModeDisabled => 'Modo de privacidad desactivado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacy => 'Configuración de privacidad';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySubtitle =>
|
||||||
|
'Controlar qué información se comparte.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySettingsDescription =>
|
||||||
|
'Elige qué información comparte tu dispositivo con otros.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_denyAll => 'Denegar todo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowByContact => 'Permitir por banderas de contacto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowAll => 'Permitir todo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryBaseMode => 'Modo base de telemetría';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryLocationMode =>
|
||||||
|
'Modo de ubicación de telemetría';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryEnvironmentMode =>
|
||||||
|
'Modo de entorno de telemetría';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocation => 'Ubicación de anuncio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocationSubtitle => 'Incluir ubicación en anuncio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_multiAck(String value) {
|
||||||
|
return 'Multi-ACKs: $value';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryModeUpdated => 'Modo de telemetría actualizado';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_actions => 'Acciones';
|
String get settings_actions => 'Acciones';
|
||||||
|
|
||||||
@@ -548,6 +739,49 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
String get appSettings_autoRouteRotationDisabled =>
|
String get appSettings_autoRouteRotationDisabled =>
|
||||||
'Rotación de ruta automática desactivada';
|
'Rotación de ruta automática desactivada';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeight => 'Peso máximo permitido para la ruta';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeightSubtitle =>
|
||||||
|
'Peso máximo que una ruta puede acumular gracias a entregas exitosas.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeight => 'Peso inicial de la ruta';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeightSubtitle =>
|
||||||
|
'Peso inicial para rutas recién descubiertas';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrement =>
|
||||||
|
'Incremento de peso para el éxito';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||||
|
'Peso añadido a una ruta después de una entrega exitosa.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrement =>
|
||||||
|
'Reducción del peso asociado al fallo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||||
|
'Peso retirado de un camino después de un intento de entrega fallido.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetries =>
|
||||||
|
'Número máximo de reintentos de envío de mensajes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetriesSubtitle =>
|
||||||
|
'Número de intentos de reintento antes de marcar un mensaje como fallido.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String path_routeWeight(String weight, String max) {
|
||||||
|
return '$weight/$max';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_battery => 'Batería';
|
String get appSettings_battery => 'Batería';
|
||||||
|
|
||||||
@@ -755,6 +989,10 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'El nombre del grupo es obligatorio';
|
String get contacts_groupNameRequired => 'El nombre del grupo es obligatorio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved =>
|
||||||
|
'Este nombre de grupo está reservado';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'El grupo \"$name\" ya existe';
|
return 'El grupo \"$name\" ya existe';
|
||||||
@@ -775,25 +1013,61 @@ 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
|
||||||
|
String get contact_info => 'Información de contacto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_settings => 'Configuración de contacto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_telemetry => 'Telemetría';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_lastSeen => 'Visto por última vez';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_clearChat => 'Borrar chat';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBase => 'Base de Telemetría';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBaseSubtitle =>
|
||||||
|
'Permitir el intercambio de nivel de batería y telemetría básica';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLoc => 'Ubicación de telemetría';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLocSubtitle =>
|
||||||
|
'Permitir el intercambio de datos de ubicación';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnv => 'Entorno de Telemetría';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnvSubtitle =>
|
||||||
|
'Permitir el intercambio de datos de sensores de entorno';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_title => 'Canales';
|
String get channels_title => 'Canales';
|
||||||
|
|
||||||
@@ -1363,6 +1637,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Compartir marcador aquí';
|
String get map_shareMarkerHere => 'Compartir marcador aquí';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_setAsMyLocation => 'Establecer mi ubicación';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Etiqueta de marcador';
|
String get map_pinLabel => 'Etiqueta de marcador';
|
||||||
|
|
||||||
@@ -1408,6 +1685,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_otherNodes => 'Otros Nodos';
|
String get map_otherNodes => 'Otros Nodos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showOverlaps => 'Superposiciones de tecla repetidora';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_keyPrefix => 'Prefijo de clave';
|
String get map_keyPrefix => 'Prefijo de clave';
|
||||||
|
|
||||||
@@ -1423,6 +1703,16 @@ 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_showDiscoveryContacts => 'Mostrar Contactos de Descubrimiento';
|
||||||
|
|
||||||
|
@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';
|
||||||
|
|
||||||
@@ -1441,6 +1731,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_runTrace => 'Ejecutar Rastreo de Ruta';
|
String get map_runTrace => 'Ejecutar Rastreo de Ruta';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTraceWithReturnPath => 'Volver atrás por el mismo camino.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_removeLast => 'Eliminar último';
|
String get map_removeLast => 'Eliminar último';
|
||||||
|
|
||||||
@@ -3113,4 +3406,85 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get snrIndicator_lastSeen => 'Visto por última vez';
|
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,137 @@ 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 connectionChoiceTcpLabel => 'TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpScreenTitle => 'Établir une connexion via TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostLabel => 'Adresse IP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostHint => '192.168.40.10';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortLabel => 'Port';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortHint => '5000';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpStatus_notConnected =>
|
||||||
|
'Entrez l\'adresse de destination et connectez-vous.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpStatus_connectingTo(String endpoint) {
|
||||||
|
return 'Connexion à $endpoint...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorHostRequired => 'Une adresse IP est obligatoire.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorPortInvalid =>
|
||||||
|
'La taille du port doit être comprise entre 1 et 65535.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorUnsupported =>
|
||||||
|
'Le protocole TCP n\'est pas pris en charge sur cette plateforme.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorTimedOut => 'La connexion TCP a expiré.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpConnectionFailed(String error) {
|
||||||
|
return 'Échec de la connexion TCP : $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Connectez via USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Sélectionnez un périphérique série détecté et connectez-vous directement à votre nœud MeshCore.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Sélectionnez un périphérique USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'La communication série USB est active sur les appareils Android et les plateformes de bureau compatibles.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'Aucun périphérique USB n\'a été trouvé. Veuillez en brancher un et rafraîchir la page.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied => 'L\'accès via USB a été refusé.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing =>
|
||||||
|
'Le périphérique USB sélectionné n\'est plus disponible.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Sélectionnez un périphérique USB valide.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy =>
|
||||||
|
'Une autre demande de connexion USB est déjà en cours.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'Aucun appareil USB n\'est connecté.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed =>
|
||||||
|
'Impossible d\'ouvrir l\'appareil USB sélectionné.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'Impossible de se connecter à l\'appareil USB sélectionné.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'La communication série USB n\'est pas prise en charge sur cette plateforme.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'Une connexion USB est déjà établie.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected =>
|
||||||
|
'Aucun appareil USB n\'a été sélectionné.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'La connexion USB n\'est pas établie.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'La connexion a expiré. Assurez-vous que l\'appareil dispose du firmware USB Companion.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName =>
|
||||||
|
'Dispositif de communication série sur le Web';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Sélectionnez un périphérique USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Connexion au périphérique USB...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Recherche de périphériques USB...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'Échec de la connexion USB : $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Recherche de périphériques...';
|
String get scanner_scanning => 'Recherche de périphériques...';
|
||||||
|
|
||||||
@@ -150,6 +284,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 +375,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é';
|
||||||
|
|
||||||
@@ -252,6 +400,52 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get settings_privacyModeDisabled =>
|
String get settings_privacyModeDisabled =>
|
||||||
'Mode de confidentialité désactivé';
|
'Mode de confidentialité désactivé';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacy => 'Paramètres de confidentialité';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySubtitle => 'Contrôlez les informations partagées';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySettingsDescription =>
|
||||||
|
'Choisissez les informations que votre appareil partage avec les autres.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_denyAll => 'Refuser tout';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowByContact => 'Autoriser par drapeaux de contact';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowAll => 'Autoriser tout';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryBaseMode => 'Mode de base Télémétrie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryLocationMode =>
|
||||||
|
'Mode d\'emplacement de télémétrie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryEnvironmentMode =>
|
||||||
|
'Mode d\'environnement de télémétrie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocation => 'Emplacement de l\'annonce';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocationSubtitle =>
|
||||||
|
'Inclure l\'emplacement dans l\'annonce';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_multiAck(String value) {
|
||||||
|
return 'Multi-ACKs : $value';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryModeUpdated =>
|
||||||
|
'Le mode télémétrie a été mis à jour';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_actions => 'Actions';
|
String get settings_actions => 'Actions';
|
||||||
|
|
||||||
@@ -550,6 +744,50 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get appSettings_autoRouteRotationDisabled =>
|
String get appSettings_autoRouteRotationDisabled =>
|
||||||
'Rotation de l\'itinéraire automatique désactivée';
|
'Rotation de l\'itinéraire automatique désactivée';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeight =>
|
||||||
|
'Poids maximal autorisé pour le trajet';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeightSubtitle =>
|
||||||
|
'Poids maximal qu\'un itinéraire peut accumuler grâce à des livraisons réussies.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeight => 'Poids initial de l\'itinéraire';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeightSubtitle =>
|
||||||
|
'Poids de départ pour les nouveaux chemins découverts';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrement =>
|
||||||
|
'Augmentation du poids de réussite';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||||
|
'Poids ajouté à un itinéraire après une livraison réussie.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrement =>
|
||||||
|
'Réduction du poids de pénalité';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||||
|
'Poids retiré d\'un itinéraire après une tentative de livraison infructueuse.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetries =>
|
||||||
|
'Nombre maximal de tentatives de récupération de messages';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetriesSubtitle =>
|
||||||
|
'Nombre de tentatives de relance avant de marquer un message comme ayant échoué.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String path_routeWeight(String weight, String max) {
|
||||||
|
return '$weight/$max';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_battery => 'Batterie';
|
String get appSettings_battery => 'Batterie';
|
||||||
|
|
||||||
@@ -757,6 +995,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'Le nom du groupe est obligatoire.';
|
String get contacts_groupNameRequired => 'Le nom du groupe est obligatoire.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => 'Ce nom de groupe est réservé';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'Le groupe \"$name\" existe déjà.';
|
return 'Le groupe \"$name\" existe déjà.';
|
||||||
@@ -777,25 +1018,61 @@ 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
|
||||||
|
String get contact_info => 'Informations de contact';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_settings => 'Paramètres de contact';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_telemetry => 'Télémétrie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_lastSeen => 'Dernière fois vu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_clearChat => 'Effacer la conversation';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBase => 'Base de télémétrie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBaseSubtitle =>
|
||||||
|
'Autoriser le partage du niveau de batterie et de la télémétrie de base';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLoc => 'Emplacement de télémétrie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLocSubtitle =>
|
||||||
|
'Autoriser le partage des données de localisation';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnv => 'Environnement Télémétrie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnvSubtitle =>
|
||||||
|
'Autoriser le partage des données des capteurs d\'environnement';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_title => 'Canaux';
|
String get channels_title => 'Canaux';
|
||||||
|
|
||||||
@@ -1370,6 +1647,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Partager le marqueur ici';
|
String get map_shareMarkerHere => 'Partager le marqueur ici';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_setAsMyLocation => 'Définir comme ma localisation';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Étiquete de repin';
|
String get map_pinLabel => 'Étiquete de repin';
|
||||||
|
|
||||||
@@ -1415,6 +1695,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_otherNodes => 'Autres nœuds';
|
String get map_otherNodes => 'Autres nœuds';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showOverlaps => 'Chevauchement de la touche répétitive';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_keyPrefix => 'Préfixe clé';
|
String get map_keyPrefix => 'Préfixe clé';
|
||||||
|
|
||||||
@@ -1430,6 +1713,16 @@ 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_showDiscoveryContacts => 'Afficher les contacts de découverte';
|
||||||
|
|
||||||
|
@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';
|
||||||
|
|
||||||
@@ -1449,6 +1742,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_runTrace => 'Exécuter la traçage de chemin';
|
String get map_runTrace => 'Exécuter la traçage de chemin';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTraceWithReturnPath => 'Revenir sur le même chemin.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_removeLast => 'Supprimer le dernier';
|
String get map_removeLast => 'Supprimer le dernier';
|
||||||
|
|
||||||
@@ -3135,4 +3431,84 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get snrIndicator_lastSeen => 'Dernière fois vu';
|
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,137 @@ 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 connectionChoiceTcpLabel => 'TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpScreenTitle => 'Stabilire una connessione tramite TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostLabel => 'Indirizzo IP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostHint => '192.168.40.10';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortLabel => 'Porta';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortHint => '5000';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpStatus_notConnected => 'Inserisci l\'endpoint e connettiti.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpStatus_connectingTo(String endpoint) {
|
||||||
|
return 'Connessione a $endpoint...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorHostRequired => 'È necessario fornire un indirizzo IP.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorPortInvalid =>
|
||||||
|
'La dimensione della porta deve essere compresa tra 1 e 65535.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorUnsupported =>
|
||||||
|
'Il protocollo TCP non è supportato su questa piattaforma.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorTimedOut => 'La connessione TCP è scaduta.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpConnectionFailed(String error) {
|
||||||
|
return 'Impossibile stabilire la connessione TCP: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Connessione tramite USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Seleziona il dispositivo seriale rilevato e connettilo direttamente al tuo nodo MeshCore.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Seleziona un dispositivo USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'Nessun dispositivo USB rilevato. Collegare uno e aggiornare.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied =>
|
||||||
|
'È stato negato l\'accesso tramite USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing =>
|
||||||
|
'Il dispositivo USB selezionato non è più disponibile.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Seleziona un dispositivo USB valido.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy =>
|
||||||
|
'Un\'altra richiesta di connessione tramite USB è già in corso.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'Non è collegato alcun dispositivo USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed =>
|
||||||
|
'Impossibile aprire il dispositivo USB selezionato.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'Impossibile connettersi al dispositivo USB selezionato.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'La comunicazione seriale tramite USB non è supportata su questa piattaforma.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'La connessione USB è già attiva.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected =>
|
||||||
|
'Non è stato selezionato alcun dispositivo USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'La connessione USB non è attiva.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName =>
|
||||||
|
'Dispositivo per comunicazione seriale su rete';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Seleziona un dispositivo USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Connessione al dispositivo USB...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Ricerca di dispositivi USB...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'Errore nella connessione USB: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Scansione in corso per i dispositivi...';
|
String get scanner_scanning => 'Scansione in corso per i dispositivi...';
|
||||||
|
|
||||||
@@ -150,6 +284,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 +374,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';
|
||||||
|
|
||||||
@@ -250,6 +398,52 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_privacyModeDisabled => 'Modalità privacy disabilitata';
|
String get settings_privacyModeDisabled => 'Modalità privacy disabilitata';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacy => 'Impostazioni sulla privacy';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySubtitle =>
|
||||||
|
'Controlla le informazioni che vengono condivise.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySettingsDescription =>
|
||||||
|
'Scegli le informazioni che il tuo dispositivo condivide con gli altri.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_denyAll => 'Negare tutto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowByContact => 'Consenti in base ai flag di contatto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowAll => 'Consenti tutto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryBaseMode => 'Modalità di base di telemetria';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryLocationMode =>
|
||||||
|
'Modalità di posizionamento telemetrico';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryEnvironmentMode =>
|
||||||
|
'Modalità di ambiente di telemetria';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocation => 'Posizione dell\'annuncio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocationSubtitle =>
|
||||||
|
'Includi la posizione nell\'annuncio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_multiAck(String value) {
|
||||||
|
return 'Multi-ACKs: $value';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryModeUpdated => 'Modalità telemetria aggiornata';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_actions => 'Azioni';
|
String get settings_actions => 'Azioni';
|
||||||
|
|
||||||
@@ -547,6 +741,50 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
String get appSettings_autoRouteRotationDisabled =>
|
String get appSettings_autoRouteRotationDisabled =>
|
||||||
'Rotazione del percorso automatico disabilitata';
|
'Rotazione del percorso automatico disabilitata';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeight =>
|
||||||
|
'Massimo peso consentito per il percorso';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeightSubtitle =>
|
||||||
|
'Il peso massimo che un percorso può accumulare grazie a consegne di successo.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeight => 'Peso iniziale del percorso';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeightSubtitle =>
|
||||||
|
'Peso di partenza per nuovi percorsi';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrement =>
|
||||||
|
'Aumento del peso del successo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||||
|
'Peso aggiunto a un percorso dopo una consegna riuscita.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrement =>
|
||||||
|
'Riduzione del peso associato al fallimento';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||||
|
'Peso rimosso da un percorso dopo un tentativo di consegna fallito.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetries =>
|
||||||
|
'Numero massimo di tentativi di invio del messaggio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetriesSubtitle =>
|
||||||
|
'Numero di tentativi di riprova prima di considerare un messaggio come fallito.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String path_routeWeight(String weight, String max) {
|
||||||
|
return '$weight/$max';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_battery => 'Batteria';
|
String get appSettings_battery => 'Batteria';
|
||||||
|
|
||||||
@@ -753,6 +991,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'Il nome del gruppo è obbligatorio.';
|
String get contacts_groupNameRequired => 'Il nome del gruppo è obbligatorio.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => 'Questo nome del gruppo è riservato';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'Il gruppo \"$name\" esiste già.';
|
return 'Il gruppo \"$name\" esiste già.';
|
||||||
@@ -792,6 +1033,42 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
return 'Ultimo visto $days giorni fa';
|
return 'Ultimo visto $days giorni fa';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_info => 'Informazioni di Contatto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_settings => 'Impostazioni di contatto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_telemetry => 'Telemetria';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_lastSeen => 'Ultimo accesso';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_clearChat => 'Cancella chat';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBase => 'Base di telemetria';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBaseSubtitle =>
|
||||||
|
'Consenti la condivisione del livello della batteria e della telemetria di base';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLoc => 'Posizione telemetria';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLocSubtitle =>
|
||||||
|
'Consenti la condivisione dei dati di posizione';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnv => 'Ambiente di telemetria';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnvSubtitle =>
|
||||||
|
'Consenti la condivisione dei dati del sensore ambientale';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_title => 'Canali';
|
String get channels_title => 'Canali';
|
||||||
|
|
||||||
@@ -1362,6 +1639,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Condividi marcatore qui';
|
String get map_shareMarkerHere => 'Condividi marcatore qui';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_setAsMyLocation => 'Imposta come la mia posizione';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Etichetta PIN';
|
String get map_pinLabel => 'Etichetta PIN';
|
||||||
|
|
||||||
@@ -1407,6 +1687,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_otherNodes => 'Altri Nodi';
|
String get map_otherNodes => 'Altri Nodi';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showOverlaps => 'Sovrapposizioni della chiave ripetitore';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_keyPrefix => 'Prefisso Chiave';
|
String get map_keyPrefix => 'Prefisso Chiave';
|
||||||
|
|
||||||
@@ -1422,6 +1705,15 @@ 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_showDiscoveryContacts => 'Mostra Contatti di Discovery';
|
||||||
|
|
||||||
|
@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';
|
||||||
|
|
||||||
@@ -1440,6 +1732,10 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_runTrace => 'Esegui Path Trace';
|
String get map_runTrace => 'Esegui Path Trace';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTraceWithReturnPath =>
|
||||||
|
'Tornare indietro sullo stesso percorso';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_removeLast => 'Rimuovi ultimo';
|
String get map_removeLast => 'Rimuovi ultimo';
|
||||||
|
|
||||||
@@ -3116,4 +3412,83 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get snrIndicator_lastSeen => 'Ultimo accesso';
|
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,134 @@ 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 connectionChoiceTcpLabel => 'TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpScreenTitle => 'Verbind via TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostLabel => 'IP-adres';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostHint => '192.168.40.10';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortLabel => 'Poort';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortHint => '5000';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpStatus_notConnected => 'Voer het eindpunt in en verbind';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpStatus_connectingTo(String endpoint) {
|
||||||
|
return 'Verbinding maken met $endpoint...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorHostRequired => 'Een IP-adres is vereist.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorPortInvalid =>
|
||||||
|
'De poortwaarde moet tussen 1 en 65535 liggen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorUnsupported =>
|
||||||
|
'TCP-transport wordt niet ondersteund op deze platform.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorTimedOut => 'De TCP-verbinding is verlopen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpConnectionFailed(String error) {
|
||||||
|
return 'Verbinding met TCP mislukt: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Verbind via USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Selecteer een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Selecteer een USB-apparaat';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'USB-serieel is actief op ondersteunde Android-apparaten en desktop-platforms.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'Geen USB-apparaten gevonden. Sluit er een aan en herlaad.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied => 'Toegang via USB is geweigerd.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing =>
|
||||||
|
'Het geselecteerde USB-apparaat is niet meer beschikbaar.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Selecteer een geldig USB-apparaat.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy =>
|
||||||
|
'Een andere verzoek om een USB-verbinding is al in behandeling.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'Er is geen USB-apparaat aangesloten.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed =>
|
||||||
|
'Kon het geselecteerde USB-apparaat niet openen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'Kon niet verbinding maken met het geselecteerde USB-apparaat.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'USB-serieel is niet ondersteund op deze platform.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'Een USB-verbinding is al actief.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected => 'Geen USB-apparaat is geselecteerd.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'De USB-verbinding is niet actief.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName => 'Web-serieapparaat';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Selecteer een USB-apparaat';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Verbinding maken met USB-apparaat...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Zoeken naar USB-apparaten...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'Fout bij de USB-verbinding: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Scannen naar apparaten...';
|
String get scanner_scanning => 'Scannen naar apparaten...';
|
||||||
|
|
||||||
@@ -149,6 +280,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 +371,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';
|
||||||
|
|
||||||
@@ -250,6 +395,50 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_privacyModeDisabled => 'Privacy modus is uitgeschakeld';
|
String get settings_privacyModeDisabled => 'Privacy modus is uitgeschakeld';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacy => 'Privacyinstellingen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySubtitle =>
|
||||||
|
'Beheer welke informatie wordt gedeeld';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySettingsDescription =>
|
||||||
|
'Kies welke informatie uw apparaat deelt met anderen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_denyAll => 'Weiger alles';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowByContact => 'Toestaan op basis van contactvlaggen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowAll => 'Alles toestaan';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryBaseMode => 'Telemetrie-basismodus';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryLocationMode => 'Telemetrie-locatiemodus';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryEnvironmentMode => 'Telemetrie-omgevingsmodus';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocation => 'Advertentielocatie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocationSubtitle =>
|
||||||
|
'Locatie opnemen in advertentie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_multiAck(String value) {
|
||||||
|
return 'Multi-ACKs: $value';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryModeUpdated => 'Telemetrie-modus bijgewerkt';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_actions => 'Acties';
|
String get settings_actions => 'Acties';
|
||||||
|
|
||||||
@@ -544,6 +733,49 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
String get appSettings_autoRouteRotationDisabled =>
|
String get appSettings_autoRouteRotationDisabled =>
|
||||||
'Automatische route rotatie is uitgeschakeld';
|
'Automatische route rotatie is uitgeschakeld';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeight => 'Maximale gewicht voor de route';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeightSubtitle =>
|
||||||
|
'Het maximale gewicht dat een route kan bereiken door succesvolle leveringen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeight => 'เริ่มต้น gewicht van de route';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeightSubtitle =>
|
||||||
|
'Startgewicht voor nieuwe, ontdekte routes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrement =>
|
||||||
|
'Toename in het gewicht van het succes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||||
|
'Gewicht wordt toegevoegd aan een route na een succesvolle levering.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrement =>
|
||||||
|
'Vermindering van het gewicht van fouten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||||
|
'Gewicht verwijderd van een pad na een mislukte levering';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetries =>
|
||||||
|
'Aantal pogingen om berichten te versturen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetriesSubtitle =>
|
||||||
|
'Aantal pogingen om een bericht opnieuw te versturen voordat het als mislukt wordt gemarkeerd';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String path_routeWeight(String weight, String max) {
|
||||||
|
return '$weight/$max';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_battery => 'Batterij';
|
String get appSettings_battery => 'Batterij';
|
||||||
|
|
||||||
@@ -750,6 +982,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'De groepnaam is verplicht.';
|
String get contacts_groupNameRequired => 'De groepnaam is verplicht.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => 'Deze groepsnaam is gereserveerd';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'De groep \"$name\" bestaat al.';
|
return 'De groep \"$name\" bestaat al.';
|
||||||
@@ -789,6 +1024,40 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
return 'Laast gezien $days dagen geleden';
|
return 'Laast gezien $days dagen geleden';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_info => 'Contactinformatie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_settings => 'Contactinstellingen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_telemetry => 'Telemetrie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_lastSeen => 'Laatst gezien';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_clearChat => 'Chat leegmaken';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBase => 'Telemetrie_basis';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBaseSubtitle =>
|
||||||
|
'Sta delen van batterij niveau en basis telemetrie toe';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLoc => 'Telemetrielocatie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLocSubtitle => 'Locatiegegevens delen toestaan';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnv => 'Telemetrieomgeving';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnvSubtitle => 'Delen van omgevingsensordata toestaan';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_title => 'Kanaal';
|
String get channels_title => 'Kanaal';
|
||||||
|
|
||||||
@@ -1357,6 +1626,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Deel marker hier';
|
String get map_shareMarkerHere => 'Deel marker hier';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_setAsMyLocation => 'Stel dit in als mijn locatie';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Label vastzetten';
|
String get map_pinLabel => 'Label vastzetten';
|
||||||
|
|
||||||
@@ -1402,6 +1674,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_otherNodes => 'Andere Nodes';
|
String get map_otherNodes => 'Andere Nodes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showOverlaps => 'Herhalingssleutel overlapt';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_keyPrefix => 'Prefix sleutel';
|
String get map_keyPrefix => 'Prefix sleutel';
|
||||||
|
|
||||||
@@ -1417,6 +1692,16 @@ 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_showDiscoveryContacts => 'Ontdek contacten weergeven';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_guessedLocation => 'Geroerde locatie';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_lastSeenTime => 'Laatste Bekeken Tijd';
|
String get map_lastSeenTime => 'Laatste Bekeken Tijd';
|
||||||
|
|
||||||
@@ -1436,6 +1721,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_runTrace => 'Padeshulp traceren';
|
String get map_runTrace => 'Padeshulp traceren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTraceWithReturnPath => 'Terugkeren op hetzelfde pad.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_removeLast => 'Verwijder Laatste';
|
String get map_removeLast => 'Verwijder Laatste';
|
||||||
|
|
||||||
@@ -3103,4 +3391,82 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get snrIndicator_lastSeen => 'Laatst gezien';
|
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?';
|
||||||
}
|
}
|
||||||
|
|||||||
+606
-219
File diff suppressed because it is too large
Load Diff
@@ -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,136 @@ 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 connectionChoiceTcpLabel => 'TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpScreenTitle => 'Estabelecer conexão via TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostLabel => 'Endereço IP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostHint => '192.168.40.10';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortLabel => 'Porta';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortHint => '5000';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpStatus_notConnected => 'Insira o endereço final e conecte-se.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpStatus_connectingTo(String endpoint) {
|
||||||
|
return 'Conectando a $endpoint...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorHostRequired => 'É necessário fornecer um endereço IP.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorPortInvalid =>
|
||||||
|
'O valor do porto deve estar entre 1 e 65535.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorUnsupported =>
|
||||||
|
'O protocolo TCP não é suportado nesta plataforma.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorTimedOut => 'A conexão TCP expirou.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpConnectionFailed(String error) {
|
||||||
|
return 'Falha na conexão TCP: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Conecte via USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Selecione um dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Selecione um dispositivo USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'A comunicação serial via USB está ativa em dispositivos Android e plataformas de desktop compatíveis.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'Nenhum dispositivo USB encontrado. Conecte um e atualize.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied =>
|
||||||
|
'A permissão para acesso via USB foi negada.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing =>
|
||||||
|
'O dispositivo USB selecionado não está mais disponível.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Selecione um dispositivo USB válido.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy =>
|
||||||
|
'Já existe uma solicitação de conexão USB em andamento.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'Não há nenhum dispositivo USB conectado.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed =>
|
||||||
|
'Não foi possível abrir o dispositivo USB selecionado.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'Não foi possível conectar ao dispositivo USB selecionado.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'A comunicação serial via USB não é suportada nesta plataforma.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'A conexão USB já está ativa.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected =>
|
||||||
|
'Nenhum dispositivo USB foi selecionado.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'A conexão USB não está ativa.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName => 'Dispositivo de Serial para a Web';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Selecione um dispositivo USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Conectando ao dispositivo USB...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Procurando por dispositivos USB...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'Falha na conexão USB: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Procurando por dispositivos...';
|
String get scanner_scanning => 'Procurando por dispositivos...';
|
||||||
|
|
||||||
@@ -150,6 +283,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 +374,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';
|
||||||
|
|
||||||
@@ -251,6 +398,51 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_privacyModeDisabled => 'Modo de privacidade desativado';
|
String get settings_privacyModeDisabled => 'Modo de privacidade desativado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacy => 'Configurações de Privacidade';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySubtitle => 'Controle o que é compartilhado.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySettingsDescription =>
|
||||||
|
'Escolha quais informações o seu dispositivo compartilha com os outros.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_denyAll => 'Negar todos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowByContact => 'Permitir por bandeiras de contato';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowAll => 'Permitir todos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryBaseMode => 'Modo Base de Telemetria';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryLocationMode =>
|
||||||
|
'Modo de Localização de Telemetria';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryEnvironmentMode =>
|
||||||
|
'Modo de Ambiente de Telemetria';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocation => 'Localização do Anúncio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocationSubtitle =>
|
||||||
|
'Incluir localização no anúncio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_multiAck(String value) {
|
||||||
|
return 'Multi-ACKs: $value';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryModeUpdated => 'Modo de telemetria atualizado';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_actions => 'Ações';
|
String get settings_actions => 'Ações';
|
||||||
|
|
||||||
@@ -549,6 +741,49 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
String get appSettings_autoRouteRotationDisabled =>
|
String get appSettings_autoRouteRotationDisabled =>
|
||||||
'Rotação de roteamento automático desativada';
|
'Rotação de roteamento automático desativada';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeight => 'Peso Máximo da Rota';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeightSubtitle =>
|
||||||
|
'Peso máximo que um determinado percurso pode acumular com entregas bem-sucedidas.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeight => 'Peso Inicial da Rota';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeightSubtitle =>
|
||||||
|
'Peso inicial para novos caminhos descobertos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrement =>
|
||||||
|
'Aumento do peso para indicar sucesso';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||||
|
'Peso adicionado a um caminho após a entrega bem-sucedida.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrement =>
|
||||||
|
'Redução do peso da falha';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||||
|
'Peso removido de um caminho após uma tentativa de entrega malsucedida.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetries =>
|
||||||
|
'Número máximo de tentativas de envio de mensagens';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetriesSubtitle =>
|
||||||
|
'Número de tentativas de reenvio antes de classificar uma mensagem como falha.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String path_routeWeight(String weight, String max) {
|
||||||
|
return '$weight/$max';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_battery => 'Bateria';
|
String get appSettings_battery => 'Bateria';
|
||||||
|
|
||||||
@@ -756,6 +991,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'O nome do grupo é obrigatório.';
|
String get contacts_groupNameRequired => 'O nome do grupo é obrigatório.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => 'Este nome de grupo está reservado';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'O grupo \"$name\" já existe';
|
return 'O grupo \"$name\" já existe';
|
||||||
@@ -795,6 +1033,42 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
return 'Última vez visto $days dias atrás';
|
return 'Última vez visto $days dias atrás';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_info => 'Informações de Contato';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_settings => 'Configurações de Contato';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_telemetry => 'Telemetria';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_lastSeen => 'Visto pela última vez';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_clearChat => 'Limpar Chat';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBase => 'Base de Telemetria';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBaseSubtitle =>
|
||||||
|
'Permitir compartilhamento do nível da bateria e telemetria básica';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLoc => 'Localização de Telemetria';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLocSubtitle =>
|
||||||
|
'Permitir compartilhamento de dados de localização';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnv => 'Ambiente de Telemetria';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnvSubtitle =>
|
||||||
|
'Permitir compartilhamento de dados do sensor de ambiente';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_title => 'Canais';
|
String get channels_title => 'Canais';
|
||||||
|
|
||||||
@@ -1364,6 +1638,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Compartilhar marcador aqui';
|
String get map_shareMarkerHere => 'Compartilhar marcador aqui';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_setAsMyLocation => 'Defina minha localização';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Rótulo de marcador';
|
String get map_pinLabel => 'Rótulo de marcador';
|
||||||
|
|
||||||
@@ -1409,6 +1686,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_otherNodes => 'Outros Nós';
|
String get map_otherNodes => 'Outros Nós';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showOverlaps => 'Sobreposições da Chave Repeater';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_keyPrefix => 'Prefixo Chave';
|
String get map_keyPrefix => 'Prefixo Chave';
|
||||||
|
|
||||||
@@ -1424,6 +1704,16 @@ 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_showDiscoveryContacts => 'Mostrar Contatos de Descoberta';
|
||||||
|
|
||||||
|
@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';
|
||||||
|
|
||||||
@@ -1442,6 +1732,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_runTrace => 'Executar Traçado de Caminho';
|
String get map_runTrace => 'Executar Traçado de Caminho';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTraceWithReturnPath => 'Retornar ao mesmo caminho.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_removeLast => 'Remover Último';
|
String get map_removeLast => 'Remover Último';
|
||||||
|
|
||||||
@@ -3111,4 +3404,84 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get snrIndicator_lastSeen => 'Visto pela última vez';
|
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,137 @@ 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 connectionChoiceTcpLabel => 'TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpScreenTitle => 'Установить соединение по протоколу TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostLabel => 'IP-адрес';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostHint => '192.168.40.10';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortLabel => 'Порт';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortHint => '5000';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpStatus_notConnected => 'Введите адрес и подключитесь.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpStatus_connectingTo(String endpoint) {
|
||||||
|
return 'Подключение к $endpoint...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorHostRequired => 'Необходимо указать IP-адрес.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorPortInvalid =>
|
||||||
|
'Порт должен находиться в диапазоне от 1 до 65535.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorUnsupported =>
|
||||||
|
'Протокол TCP не поддерживается на этой платформе.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorTimedOut => 'Соединение TCP не удалось установить.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpConnectionFailed(String error) {
|
||||||
|
return 'Не удалось установить соединение TCP: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Подключение через USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Выберите обнаруженное устройство с последовательным интерфейсом и подключите его напрямую к вашему узлу MeshCore.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Выберите USB-устройство';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'USB-серийный порт активен на поддерживаемых устройствах Android и на настольных платформах.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'Не обнаружено устройств USB. Подключите одно из них и обновите список.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied =>
|
||||||
|
'Запрос на доступ через USB был отклонен.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing =>
|
||||||
|
'Выбранное USB-устройство больше недоступно.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Выберите действительное USB-устройство.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy =>
|
||||||
|
'Еще одно запрошенное соединение через USB уже находится в процессе.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'Ни одно USB-устройство не подключено.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed =>
|
||||||
|
'Не удалось открыть выбранное USB-устройство.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'Не удалось установить соединение с выбранным USB-устройством.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'Поддержка последовательного USB отсутствует на данной платформе.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'USB-соединение уже установлено.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected =>
|
||||||
|
'Не было выбрано ни одно устройство USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'USB-соединение не установлено.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName =>
|
||||||
|
'Устройство для последовательного подключения к сети';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Выберите USB-устройство';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Подключение к USB-устройству...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Поиск USB-устройств...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'Не удалось установить соединение через USB: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Поиск устройств...';
|
String get scanner_scanning => 'Поиск устройств...';
|
||||||
|
|
||||||
@@ -149,6 +283,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 +373,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 => 'Режим конфиденциальности';
|
||||||
|
|
||||||
@@ -250,6 +398,51 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
String get settings_privacyModeDisabled =>
|
String get settings_privacyModeDisabled =>
|
||||||
'Режим конфиденциальности выключен';
|
'Режим конфиденциальности выключен';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacy => 'Настройки конфиденциальности';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySubtitle =>
|
||||||
|
'Контролируйте, какую информацию делиться.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySettingsDescription =>
|
||||||
|
'Выберите, какую информацию ваше устройство будет делиться с другими.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_denyAll => 'Отклонить все';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowByContact => 'Разрешить по флагам контактов';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowAll => 'Разрешить все';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryBaseMode => 'Базовый режим телеметрии';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryLocationMode =>
|
||||||
|
'Режим местоположения телеметрии';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryEnvironmentMode => 'Режим среды телеметрии';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocation => 'Местоположение рекламы';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocationSubtitle =>
|
||||||
|
'Включить местоположение в объявление';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_multiAck(String value) {
|
||||||
|
return 'Мульти-ACK: $value';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryModeUpdated => 'Режим телеметрии обновлен';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_actions => 'Действия';
|
String get settings_actions => 'Действия';
|
||||||
|
|
||||||
@@ -548,6 +741,50 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
String get appSettings_autoRouteRotationDisabled =>
|
String get appSettings_autoRouteRotationDisabled =>
|
||||||
'Автоматическое переключение маршрутов отключено';
|
'Автоматическое переключение маршрутов отключено';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeight =>
|
||||||
|
'Максимальный допустимый вес маршрута';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeightSubtitle =>
|
||||||
|
'Максимальный вес, который может быть перевезён по определённому маршруту при успешных доставках.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeight => 'Начальный вес маршрута';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeightSubtitle =>
|
||||||
|
'Начальный вес для новых, только что открытых маршрутов';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrement =>
|
||||||
|
'Увеличение веса успеха';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||||
|
'Вес, добавленный к маршруту после успешной доставки.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrement =>
|
||||||
|
'Уменьшение веса неудачи';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||||
|
'Вес, который был удален с пути после неудачной доставки.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetries =>
|
||||||
|
'Максимальное количество повторных попыток отправки сообщения';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetriesSubtitle =>
|
||||||
|
'Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String path_routeWeight(String weight, String max) {
|
||||||
|
return '$weight/$max';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_battery => 'Батарея';
|
String get appSettings_battery => 'Батарея';
|
||||||
|
|
||||||
@@ -754,6 +991,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'Имя группы обязательно';
|
String get contacts_groupNameRequired => 'Имя группы обязательно';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => 'Это имя группы зарезервировано';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'Группа \"$name\" уже существует';
|
return 'Группа \"$name\" уже существует';
|
||||||
@@ -793,6 +1033,42 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
return 'Видели $days дн. назад';
|
return 'Видели $days дн. назад';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_info => 'Контактная информация';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_settings => 'Настройки контактов';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_telemetry => 'Телеметрия';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_lastSeen => 'Последний раз видели';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_clearChat => 'Очистить чат';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBase => 'База телеметрии';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBaseSubtitle =>
|
||||||
|
'Разрешить обмен уровнем заряда батареи и базовой телеметрией';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLoc => 'Местоположение телеметрии';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLocSubtitle =>
|
||||||
|
'Разрешить обмен данными о местоположении';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnv => 'Среда телеметрии';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnvSubtitle =>
|
||||||
|
'Разрешить обмен данными датчиков окружающей среды';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_title => 'Каналы';
|
String get channels_title => 'Каналы';
|
||||||
|
|
||||||
@@ -1365,6 +1641,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Поделиться меткой здесь';
|
String get map_shareMarkerHere => 'Поделиться меткой здесь';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_setAsMyLocation => 'Установить мое местоположение';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Метка';
|
String get map_pinLabel => 'Метка';
|
||||||
|
|
||||||
@@ -1410,6 +1689,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_otherNodes => 'Другие ноды';
|
String get map_otherNodes => 'Другие ноды';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showOverlaps => 'Перекрытия ключа повтора';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_keyPrefix => 'Префикс ключа';
|
String get map_keyPrefix => 'Префикс ключа';
|
||||||
|
|
||||||
@@ -1425,6 +1707,16 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_showSharedMarkers => 'Показывать общие метки';
|
String get map_showSharedMarkers => 'Показывать общие метки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showGuessedLocations =>
|
||||||
|
'Отобразить предполагаемые места расположения узлов';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showDiscoveryContacts => 'Показать контакты Discovery';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_guessedLocation => 'Угаданное место';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_lastSeenTime => 'Время последнего появления';
|
String get map_lastSeenTime => 'Время последнего появления';
|
||||||
|
|
||||||
@@ -1443,6 +1735,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_runTrace => 'Запустить трассировку пути';
|
String get map_runTrace => 'Запустить трассировку пути';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTraceWithReturnPath => 'Вернуться обратно по тому же пути';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_removeLast => 'Удалить последний';
|
String get map_removeLast => 'Удалить последний';
|
||||||
|
|
||||||
@@ -3123,4 +3418,84 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get snrIndicator_lastSeen => 'Последний раз видели';
|
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,135 @@ 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 connectionChoiceTcpLabel => 'TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpScreenTitle => 'Spojte sa pomocou protokolu TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostLabel => 'IP adresa';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostHint => '192.168.40.10';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortLabel => 'Port';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortHint => '5000';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpStatus_notConnected => 'Zadajte cieľovú adresu a pripojte sa.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpStatus_connectingTo(String endpoint) {
|
||||||
|
return 'Pripojenie k $endpoint...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorHostRequired => 'Je potrebné zadať IP adresu.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorPortInvalid => 'Číslo portu musí byť medzi 1 a 65535.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorUnsupported =>
|
||||||
|
'Prevoz prostredníctvom protokolu TCP nie je na tejto platforme podporovaný.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorTimedOut => 'Pripojenie TCP vypršalo.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpConnectionFailed(String error) {
|
||||||
|
return 'Neúspešné vytvorenie TCP spojenia: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Pripojte cez USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vašej MeshCore uzlu.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Vyberte USB zariadenie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied =>
|
||||||
|
'Žiadosť o prístup cez USB bola zamietnutá.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing =>
|
||||||
|
'Vybrané USB zariadenie už nie je dostupné.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Vyberte platné USB zariadenie.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy =>
|
||||||
|
'Ďalšia požiadavka na pripojenie cez USB je aktuálne v procese.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'Nie je pripojené žiadne USB zariadenie.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed =>
|
||||||
|
'Nepodarilo sa otvoriť vybrané USB zariadenie.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'Nepodarilo sa sa sa pripojiť k vybranému USB zariadeniu.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'Podpora USB sériového rozhrania nie je na tejto platforme dostupná.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'Pripojenie cez USB je už aktivované.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected =>
|
||||||
|
'Nebolo vybrané žiadne USB zariadenie.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'Pripojenie cez USB nie je aktivované.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName => 'Webový sériový zariadenie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Vyberte USB zariadenie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Pripojenie k USB zariadeniu...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Hľadanie USB zariadení...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'Neúspešné pripojenie cez USB: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Skrívania zariadení...';
|
String get scanner_scanning => 'Skrívania zariadení...';
|
||||||
|
|
||||||
@@ -150,6 +282,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 +372,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';
|
||||||
|
|
||||||
@@ -249,6 +395,49 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_privacyModeDisabled => 'Ochranný režim je vypnutý';
|
String get settings_privacyModeDisabled => 'Ochranný režim je vypnutý';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacy => 'Nastavenia súkromia';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySubtitle => 'Ovládni, aké informácie sa zdieľajú.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySettingsDescription =>
|
||||||
|
'Vyberte, ktoré informácie váš zariadenie zdieľa s ostatnými.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_denyAll => 'Zamietnuť všetko';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowByContact => 'Povoliť podľa kontaktových vlajok';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowAll => 'Povoliť všetko';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryBaseMode => 'Základný režim telemetrie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryLocationMode => 'Režim umiestnenia telemetrie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryEnvironmentMode => 'Režim prostredia telemetrie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocation => 'Umiestnenie inzerátu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocationSubtitle => 'Zahrnúť polohu do inzerátu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_multiAck(String value) {
|
||||||
|
return 'Viaceré ACK: $value';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryModeUpdated =>
|
||||||
|
'Režim telemetrie bol aktualizovaný';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_actions => 'Možné akcie';
|
String get settings_actions => 'Možné akcie';
|
||||||
|
|
||||||
@@ -541,6 +730,48 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
String get appSettings_autoRouteRotationDisabled =>
|
String get appSettings_autoRouteRotationDisabled =>
|
||||||
'Automatické prekladanie trás pozastavené';
|
'Automatické prekladanie trás pozastavené';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeight => 'Maximálna hmotnosť trasy';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeightSubtitle =>
|
||||||
|
'Maximálna hmotnosť, ktorú môže trás prenášať vďaka úspešným zásielkam.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeight => 'Počiatočná váha trasy';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeightSubtitle =>
|
||||||
|
'Počiatočná váha pre nové, objavené cesty';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrement => 'Zvyšenie váhy úspechu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||||
|
'Hmotnosť pridaná k trase po úspešnej doručení';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrement =>
|
||||||
|
'Sníženie váhy, ktorá sa používa na odhad rizika.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||||
|
'Hmotnosť odstránená z cesty po neúspešnej doručenie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetries =>
|
||||||
|
'Maximalný počet pokusov o doručenie správ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetriesSubtitle =>
|
||||||
|
'Počet pokusov o odošleť pred označením správy ako neúspešnej';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String path_routeWeight(String weight, String max) {
|
||||||
|
return '$weight/$max';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_battery => 'Batéria';
|
String get appSettings_battery => 'Batéria';
|
||||||
|
|
||||||
@@ -748,6 +979,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'Skupina musí mať názov.';
|
String get contacts_groupNameRequired => 'Skupina musí mať názov.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => 'Tento názov skupiny je rezervovaný';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'Skupina \"$name\" už existuje';
|
return 'Skupina \"$name\" už existuje';
|
||||||
@@ -789,6 +1023,41 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
return 'Posledné zobrazenie $days dní dozadu';
|
return 'Posledné zobrazenie $days dní dozadu';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_info => 'Kontaktné informácie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_settings => 'Nastavenia kontaktov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_telemetry => 'Telemetria';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_lastSeen => 'Naposledy videný';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_clearChat => 'Vymazať chat';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBase => 'Báza telemetrie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBaseSubtitle =>
|
||||||
|
'Povoliť zdieľanie úrovne batérie a základnej telemetrie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLoc => 'Lokácia telemetrie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLocSubtitle => 'Povoliť zdieľanie údajov o lokalite';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnv => 'Prostredie telemetrie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnvSubtitle =>
|
||||||
|
'Povoliť zdieľanie údajov senzorov prostredia';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_title => 'Kanály';
|
String get channels_title => 'Kanály';
|
||||||
|
|
||||||
@@ -1358,6 +1627,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Zdieľte značku tu';
|
String get map_shareMarkerHere => 'Zdieľte značku tu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_setAsMyLocation => 'Nastavte ako moju polohu';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Označka upozornenia';
|
String get map_pinLabel => 'Označka upozornenia';
|
||||||
|
|
||||||
@@ -1403,6 +1675,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_otherNodes => 'Ostatné uzly';
|
String get map_otherNodes => 'Ostatné uzly';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showOverlaps => 'Prekrývanie opakovača kľúča';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_keyPrefix => 'Päťciferné predpona';
|
String get map_keyPrefix => 'Päťciferné predpona';
|
||||||
|
|
||||||
@@ -1418,6 +1693,16 @@ 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_showDiscoveryContacts => 'Zobraziť kontakty objavov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_guessedLocation => 'Odhadnutá lokalita';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_lastSeenTime => 'Posledný čas sledovania';
|
String get map_lastSeenTime => 'Posledný čas sledovania';
|
||||||
|
|
||||||
@@ -1436,6 +1721,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_runTrace => 'Spustiť trasovaním cesty';
|
String get map_runTrace => 'Spustiť trasovaním cesty';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTraceWithReturnPath => 'Vráťte sa späť po tej istej ceste.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_removeLast => 'Odstrániť posledný';
|
String get map_removeLast => 'Odstrániť posledný';
|
||||||
|
|
||||||
@@ -3098,4 +3386,82 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get snrIndicator_lastSeen => 'Naposledy videný';
|
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,133 @@ 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 connectionChoiceTcpLabel => 'TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpScreenTitle => 'Komunicirajte preko protokola TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostLabel => 'IP naslov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostHint => '192.168.40.10';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortLabel => 'Vrata';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortHint => '5000';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpStatus_notConnected => 'Vnesite končni naslov in se povežite';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpStatus_connectingTo(String endpoint) {
|
||||||
|
return 'Povezava z $endpoint...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorHostRequired => 'Potrebna je IP-naslov.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorPortInvalid => 'Port mora biti med 1 in 65535.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorUnsupported =>
|
||||||
|
'Transport preko protokola TCP ni podprt na tej platformi.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorTimedOut => 'Povezava TCP je presegla časovno obdobje.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpConnectionFailed(String error) {
|
||||||
|
return 'Napaka pri povezavi TCP: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Povežite preko USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Izberite zaznano serijsko napravo in se neposredno povežite z vašo MeshCore napravo.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Izberite USB naprave';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'USB serijska povezava je aktivna na podprtih napravah Android in na desktop platformah.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'Niti en USB naprave niso najdeni. Povežite eno in posodobite.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied =>
|
||||||
|
'Dovoljenje za dostop preko USB-ja je bilo zavrnjeno.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing => 'Izbrani USB napravej je več ne.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Izberite veljavno USB naprave.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy => 'Že je v teku zahteva za povezavo preko USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'Ni priklopljenih USB naprave.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed =>
|
||||||
|
'Uspešno ni bilo mogo, da se odpre izbran naprave USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'Niso bilo mogoče uskladiti povezave z izbranim USB napom.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'USB serijska komunikacija ni podprta na tej platformi.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'USB povezava je že aktivirana.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected => 'Ni bilo izbranega USB naprave.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'USB povezava ni aktivirana.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName =>
|
||||||
|
'Naprave za serijsko komunikacijo preko spleta';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Izberite USB naprave.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Povezava z USB napravo...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Iskanje USB naprav...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'Napaka pri povezavi preko USB: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Skeniram za naprave...';
|
String get scanner_scanning => 'Skeniram za naprave...';
|
||||||
|
|
||||||
@@ -150,6 +280,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 +370,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';
|
||||||
|
|
||||||
@@ -249,6 +393,50 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_privacyModeDisabled => 'Privatni način je onemogočen.';
|
String get settings_privacyModeDisabled => 'Privatni način je onemogočen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacy => 'Nastavitve zasebnosti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySubtitle =>
|
||||||
|
'Kontrolirajte, katere informacije so deljene.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySettingsDescription =>
|
||||||
|
'Izberite, katere informacije vaš naprava deli z drugimi.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_denyAll => 'Zavrniti vse';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowByContact => 'Dovoli po kontaktnih zastavah';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowAll => 'Dovoli vse';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryBaseMode => 'Osnovni način telemetrije';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryLocationMode => 'Način delovanja telemetrije';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryEnvironmentMode =>
|
||||||
|
'Način delovanja okolja telemetrije';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocation => 'Lokacija oglasa';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocationSubtitle => 'Vključi lokacijo v oglas.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_multiAck(String value) {
|
||||||
|
return 'Večkratni potrditvi: $value';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryModeUpdated => 'Način telemetrije posodobljen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_actions => 'Akcije';
|
String get settings_actions => 'Akcije';
|
||||||
|
|
||||||
@@ -543,6 +731,49 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
String get appSettings_autoRouteRotationDisabled =>
|
String get appSettings_autoRouteRotationDisabled =>
|
||||||
'Samodejno krmilno rotiranje je onemogočeno';
|
'Samodejno krmilno rotiranje je onemogočeno';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeight => 'Največja dovoljena teža poti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeightSubtitle =>
|
||||||
|
'Največja teža, ki jo lahko pot doseže s uspešnimi dostavnami.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeight => 'Izvirna teža poti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeightSubtitle =>
|
||||||
|
'Izguba teže za nove, odkriti poti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrement =>
|
||||||
|
'Učinkovitost: povečanje';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||||
|
'Težava, dodana poti po uspešni dostavi';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrement =>
|
||||||
|
'Zmanjšanje teže, ki je povezana s pomanjkanjem';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||||
|
'Težo, ki ni bila uspešno dostavljena, odstranili s poti.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetries =>
|
||||||
|
'Najve število poskusov pošiljanja sporočil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetriesSubtitle =>
|
||||||
|
'Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String path_routeWeight(String weight, String max) {
|
||||||
|
return '$weight/$max';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_battery => 'Baterija';
|
String get appSettings_battery => 'Baterija';
|
||||||
|
|
||||||
@@ -748,6 +979,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'Ime skupine je obvezno.';
|
String get contacts_groupNameRequired => 'Ime skupine je obvezno.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => 'To ime skupine je rezervirano';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'Skupina \"$name\" že obstaja';
|
return 'Skupina \"$name\" že obstaja';
|
||||||
@@ -787,6 +1021,41 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
return 'Zadnjič viden pred $days dnem';
|
return 'Zadnjič viden pred $days dnem';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_info => 'Kontaktni podatki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_settings => 'Nastavitve stika';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_telemetry => 'Telemetrija';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_lastSeen => 'Zadnjič videno';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_clearChat => 'Počisti klepet';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBase => 'Baza telemetrije';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBaseSubtitle =>
|
||||||
|
'Dovoli deljenje stanja baterije in osnovne telemetrije';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLoc => 'Lokacija telemetrije';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLocSubtitle => 'Dovoli deljenje podatkov o lokaciji';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnv => 'Okolje telemetrije';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnvSubtitle =>
|
||||||
|
'Dovoli deljenje podatkov okoljskih senzorjev';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_title => 'Kanali';
|
String get channels_title => 'Kanali';
|
||||||
|
|
||||||
@@ -1354,6 +1623,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Delite točke tukaj.';
|
String get map_shareMarkerHere => 'Delite točke tukaj.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_setAsMyLocation => 'Nastavite to kot mojo lokacijo';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Oznaka za pritrditev';
|
String get map_pinLabel => 'Oznaka za pritrditev';
|
||||||
|
|
||||||
@@ -1399,6 +1671,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_otherNodes => 'Druge vozlišča';
|
String get map_otherNodes => 'Druge vozlišča';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showOverlaps => 'Prekrivanje ključa ponovnega predvajanja';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_keyPrefix => 'Predpona ključa';
|
String get map_keyPrefix => 'Predpona ključa';
|
||||||
|
|
||||||
@@ -1414,6 +1689,15 @@ 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_showDiscoveryContacts => 'Prikaži odkritja kontaktov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_guessedLocation => 'Predpostavljena lokacija';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_lastSeenTime => 'Datum zadnjega vpogleda';
|
String get map_lastSeenTime => 'Datum zadnjega vpogleda';
|
||||||
|
|
||||||
@@ -1432,6 +1716,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_runTrace => 'Zaženi sledenje poti';
|
String get map_runTrace => 'Zaženi sledenje poti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTraceWithReturnPath => 'Vrni se nazaj po isti poti.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_removeLast => 'Odstrani Zadnji';
|
String get map_removeLast => 'Odstrani Zadnji';
|
||||||
|
|
||||||
@@ -3103,4 +3390,81 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get snrIndicator_lastSeen => 'Zadnjič videno';
|
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,133 @@ 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 connectionChoiceTcpLabel => 'TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpScreenTitle => 'Anslut via TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostLabel => 'IP-adress';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostHint => '192.168.40.10';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortLabel => 'Port';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortHint => '5000';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpStatus_notConnected => 'Ange slutpunkt och anslut';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpStatus_connectingTo(String endpoint) {
|
||||||
|
return 'Anslutning till $endpoint...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorHostRequired => 'IP-adress krävs.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorPortInvalid => 'Porten måste vara mellan 1 och 65535.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorUnsupported =>
|
||||||
|
'TCP-transport fungerar inte på denna plattform.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorTimedOut => 'TCP-anslutningen har tidsut gått.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpConnectionFailed(String error) {
|
||||||
|
return 'Fel vid TCP-anslutning: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Anslut via USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Välj en USB-enhet';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'USB-seriell kommunikation är aktiv på stödda Android-enheter och på skrivbordsplattformar.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'Inga USB-enheter hittades. Anslut en och uppdatera.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied => 'Tillgången via USB nekas.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing =>
|
||||||
|
'Den valda USB-enheten är inte längre tillgänglig.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Välj en giltig USB-enhet.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy =>
|
||||||
|
'En annan förfrågan om USB-anslutning är redan pågående.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'Ingen USB-enhet är ansluten.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed =>
|
||||||
|
'Misslyckades med att öppna det valda USB-enheten.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'Kunde inte ansluta till det valda USB-enheten.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'USB-seriell kommunikation stöds inte på denna plattform.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'En USB-anslutning är redan aktiv.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected => 'Ingen USB-enhet valdes.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'USB-anslutningen är inte aktiv.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName => 'Web-serieenhet';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Välj en USB-enhet';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Anslutning till USB-enhet...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Söker efter USB-enheter...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'Fel vid USB-anslutning: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Söker efter enheter...';
|
String get scanner_scanning => 'Söker efter enheter...';
|
||||||
|
|
||||||
@@ -149,6 +279,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 +369,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';
|
||||||
|
|
||||||
@@ -248,6 +392,49 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_privacyModeDisabled => 'Privatläge är avstängt';
|
String get settings_privacyModeDisabled => 'Privatläge är avstängt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacy => 'Inställningar för sekretess';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySubtitle =>
|
||||||
|
'Kontrollera vilken information som delas.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySettingsDescription =>
|
||||||
|
'Välj vilken information din enhet delar med andra.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_denyAll => 'Neka alla';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowByContact => 'Tillåt via kontaktflaggor';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowAll => 'Tillåt alla';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryBaseMode => 'Telemetribasläge';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryLocationMode => 'Telemetritillstånd för plats';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryEnvironmentMode => 'Telemetri miljöläge';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocation => 'Annonsplacering';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocationSubtitle => 'Inkludera plats i annonsen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_multiAck(String value) {
|
||||||
|
return 'Multi-ACKs: $value';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryModeUpdated => 'Telemetri-läge uppdaterat';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_actions => 'Åtgärder';
|
String get settings_actions => 'Åtgärder';
|
||||||
|
|
||||||
@@ -538,6 +725,48 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
String get appSettings_autoRouteRotationDisabled =>
|
String get appSettings_autoRouteRotationDisabled =>
|
||||||
'Automatisk ruttrotation är avstängd';
|
'Automatisk ruttrotation är avstängd';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeight => 'Maximalt tillåtet vikt för rutten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeightSubtitle =>
|
||||||
|
'Maximal vikt som en leveransväg kan ackumulera från framgångsrika leveranser.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeight => 'Initial vikt för rutt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeightSubtitle =>
|
||||||
|
'Initial vikt för nyligen upptäckta vägar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrement =>
|
||||||
|
'Ökning av vikt för framgång';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||||
|
'Vikt läggs till en väg efter en lyckad leverans.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrement =>
|
||||||
|
'Minskning av vikten för misslyckande';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||||
|
'Vikt som tagits bort från en väg efter ett misslyckat leveransförsök';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetries => 'Maximalt antal försök';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetriesSubtitle =>
|
||||||
|
'Antal försök att skicka om ett meddelande innan det markeras som misslyckat.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String path_routeWeight(String weight, String max) {
|
||||||
|
return '$weight/$max';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_battery => 'Batteri';
|
String get appSettings_battery => 'Batteri';
|
||||||
|
|
||||||
@@ -744,6 +973,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'Gruppnamnet är obligatoriskt';
|
String get contacts_groupNameRequired => 'Gruppnamnet är obligatoriskt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => 'Detta gruppnamn är reserverat';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'Gruppen \"$name\" finns redan.';
|
return 'Gruppen \"$name\" finns redan.';
|
||||||
@@ -783,6 +1015,40 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
return 'Senast synlig $days dagar sedan';
|
return 'Senast synlig $days dagar sedan';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_info => 'Kontaktinformation';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_settings => 'Kontaktinställningar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_telemetry => 'Telemetri';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_lastSeen => 'Senast sedd';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_clearChat => 'Rensa Chatt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBase => 'Telemetribas';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBaseSubtitle =>
|
||||||
|
'Tillåt delning av batterinivå och grundläggande telemetri';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLoc => 'Telemetridata plats';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLocSubtitle => 'Tillåt delning av platsdata';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnv => 'Telemetri Miljö';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnvSubtitle => 'Tillåt delning av miljösensordata';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_title => 'Kanaler';
|
String get channels_title => 'Kanaler';
|
||||||
|
|
||||||
@@ -1350,6 +1616,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Dela markeringen här';
|
String get map_shareMarkerHere => 'Dela markeringen här';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_setAsMyLocation => 'Ange som min plats';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Fästetikett';
|
String get map_pinLabel => 'Fästetikett';
|
||||||
|
|
||||||
@@ -1395,6 +1664,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_otherNodes => 'Andra noder';
|
String get map_otherNodes => 'Andra noder';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showOverlaps => 'Repeater-nyckelöverlappningar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_keyPrefix => 'Nyckelprefix';
|
String get map_keyPrefix => 'Nyckelprefix';
|
||||||
|
|
||||||
@@ -1410,6 +1682,16 @@ 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_showDiscoveryContacts => 'Visa Discovery-kontakter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_guessedLocation => 'Gissad plats';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_lastSeenTime => 'Senaste Visats Tid';
|
String get map_lastSeenTime => 'Senaste Visats Tid';
|
||||||
|
|
||||||
@@ -1428,6 +1710,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_runTrace => 'Kör spårsökning';
|
String get map_runTrace => 'Kör spårsökning';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTraceWithReturnPath => 'Gå tillbaka på samma väg';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_removeLast => 'Ta bort sista';
|
String get map_removeLast => 'Ta bort sista';
|
||||||
|
|
||||||
@@ -3081,4 +3366,82 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get snrIndicator_lastSeen => 'Senast sedd';
|
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,135 @@ 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 connectionChoiceTcpLabel => 'TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpScreenTitle => 'З\'єднатися через протокол TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostLabel => 'IP-адреса';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostHint => '192.168.40.10';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortLabel => 'Порт';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortHint => '5000';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpStatus_notConnected => 'Введіть кінцеву точку та підключіться';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpStatus_connectingTo(String endpoint) {
|
||||||
|
return 'Підключення до $endpoint...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorHostRequired => 'Необхідно вказати IP-адресу.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorPortInvalid => 'Порт повинен бути в межах від 1 до 65535.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorUnsupported =>
|
||||||
|
'Транспорт TCP не підтримується на цій платформі.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorTimedOut =>
|
||||||
|
'З\'єднання TCP завершилося через закінчення часу очікування.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpConnectionFailed(String error) {
|
||||||
|
return 'Не вдалося встановити з\'єднання TCP: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => 'Підключити через USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle =>
|
||||||
|
'Виберіть виявлене серійне пристрій і підключіть його безпосередньо до вашого вузла MeshCore.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => 'Виберіть пристрій USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote =>
|
||||||
|
'USB-серіальний інтерфейс активний на підтримуваних пристроях на базі Android та на десктопних платформах.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState =>
|
||||||
|
'Не знайдено жодних пристроїв USB. Підключіть один і перезавантажте.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied =>
|
||||||
|
'Було відмовлено у наданні дозволу на використання USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing => 'Вибране USB-пристрій більше недоступне.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => 'Виберіть дійсний USB-пристрій.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy =>
|
||||||
|
'Ще один запит на підключення через USB вже обробляється.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => 'Немає підключених пристроїв USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed => 'Не вдалося відкрити вибране USB-пристрій.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed =>
|
||||||
|
'Не вдалося підключитися до вибраного USB-пристрою.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported =>
|
||||||
|
'Підтримка USB-серіального інтерфейсу не реалізована на цій платформі.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'USB-з\'єднання вже встановлено.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected =>
|
||||||
|
'Не було вибрано жодного пристрою USB.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'З\'єднання USB не встановлено.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut =>
|
||||||
|
'З\'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName =>
|
||||||
|
'Пристрій для передачі даних по веб-серіалах';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => 'Виберіть пристрій USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => 'Підключення до USB-пристрою...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => 'Пошук пристроїв USB...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'Не вдалося встановити з\'єднання через USB: $error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => 'Пошук пристроїв...';
|
String get scanner_scanning => 'Пошук пристроїв...';
|
||||||
|
|
||||||
@@ -150,6 +282,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 +371,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 => 'Режим приватності';
|
||||||
|
|
||||||
@@ -249,6 +395,50 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_privacyModeDisabled => 'Режим приватності вимкнено';
|
String get settings_privacyModeDisabled => 'Режим приватності вимкнено';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacy => 'Налаштування приватності';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySubtitle =>
|
||||||
|
'Керуйте інформацією, яку буде спільно використовуватися';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySettingsDescription =>
|
||||||
|
'Виберіть, яку інформацію ваш пристрій буде передавати іншим.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_denyAll => 'Відхилити все';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowByContact => 'Дозволити за контактними прапорцями';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowAll => 'Дозволити все';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryBaseMode => 'Режим базової телеметрії';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryLocationMode => 'Режим місця телеметрії';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryEnvironmentMode => 'Режим середовища телеметрії';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocation => 'Розміщення реклами';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocationSubtitle =>
|
||||||
|
'Включити місце розташування в оголошення';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_multiAck(String value) {
|
||||||
|
return 'Багатократне підтвердження: $value';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryModeUpdated => 'Режим телеметрії оновлено';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_actions => 'Дії';
|
String get settings_actions => 'Дії';
|
||||||
|
|
||||||
@@ -546,6 +736,49 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
String get appSettings_autoRouteRotationDisabled =>
|
String get appSettings_autoRouteRotationDisabled =>
|
||||||
'Авторотація маршрутизації вимкнена';
|
'Авторотація маршрутизації вимкнена';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeight => 'Максимальна вага маршруту';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeightSubtitle =>
|
||||||
|
'Максимальна вага, яку може накопичити маршрут завдяки успішним доставкам.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeight => 'Початкова вартість маршруту';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeightSubtitle =>
|
||||||
|
'Початкова вага для нових відкритих шляхів';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrement =>
|
||||||
|
'Збільшення ваги успіху';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||||
|
'Вага, додана до маршруту після успішної доставки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrement =>
|
||||||
|
'Зменшення ваги помилки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||||
|
'Вага, яка була знята з маршруту після невдалої доставки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetries =>
|
||||||
|
'Максимальна кількість повторних спроб надсилання повідомлення';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetriesSubtitle =>
|
||||||
|
'Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String path_routeWeight(String weight, String max) {
|
||||||
|
return '$weight/$max';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_battery => 'Батарея';
|
String get appSettings_battery => 'Батарея';
|
||||||
|
|
||||||
@@ -752,6 +985,9 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => 'Назва групи обов\'язкова.';
|
String get contacts_groupNameRequired => 'Назва групи обов\'язкова.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => 'Ця назва групи зарезервована';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return 'Група «$name» вже існує.';
|
return 'Група «$name» вже існує.';
|
||||||
@@ -791,6 +1027,42 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
return 'В мережі $days дн. тому';
|
return 'В мережі $days дн. тому';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_info => 'Контактна інформація';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_settings => 'Налаштування контактів';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_telemetry => 'Телеметрія';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_lastSeen => 'Останній раз бачили';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_clearChat => 'Очистити чат';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBase => 'Базовий телебачення';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBaseSubtitle =>
|
||||||
|
'Дозволити спільний доступ до рівня заряду батареї та базової телеметрії';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLoc => 'Розташування телеметрії';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLocSubtitle =>
|
||||||
|
'Дозволити спільне використання даних про місцеположення';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnv => 'Середовище телеметрії';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnvSubtitle =>
|
||||||
|
'Дозволити спільний доступ до даних датчиків середовища';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_title => 'Канали';
|
String get channels_title => 'Канали';
|
||||||
|
|
||||||
@@ -1364,6 +1636,9 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => 'Поділитися маркером тут';
|
String get map_shareMarkerHere => 'Поділитися маркером тут';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_setAsMyLocation => 'Встановити моє місцезнаходження';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => 'Мітка піна';
|
String get map_pinLabel => 'Мітка піна';
|
||||||
|
|
||||||
@@ -1409,6 +1684,9 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_otherNodes => 'Інші вузли';
|
String get map_otherNodes => 'Інші вузли';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showOverlaps => 'Перекриття ключа повторювача';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_keyPrefix => 'Префікс ключа';
|
String get map_keyPrefix => 'Префікс ключа';
|
||||||
|
|
||||||
@@ -1424,6 +1702,16 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_showSharedMarkers => 'Показувати спільні маркери';
|
String get map_showSharedMarkers => 'Показувати спільні маркери';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showGuessedLocations =>
|
||||||
|
'Показати місцезнаходження передбачених вузлів';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showDiscoveryContacts => 'Показати контакти Відкриття';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_guessedLocation => 'Визначено місцезнаходження';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_lastSeenTime => 'Час останньої активності';
|
String get map_lastSeenTime => 'Час останньої активності';
|
||||||
|
|
||||||
@@ -1442,6 +1730,9 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_runTrace => 'Виконати трасування шляху';
|
String get map_runTrace => 'Виконати трасування шляху';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTraceWithReturnPath => 'Повернутися назад тим же шляхом';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_removeLast => 'Видалити останній';
|
String get map_removeLast => 'Видалити останній';
|
||||||
|
|
||||||
@@ -3130,4 +3421,84 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get snrIndicator_lastSeen => 'Останній раз бачили';
|
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 AppLocalizationsZh 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,123 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_title => '连接设备';
|
String get scanner_title => '连接设备';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceUsbLabel => 'USB';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceBluetoothLabel => '蓝牙';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get connectionChoiceTcpLabel => 'TCP';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpScreenTitle => '通过 TCP 连接';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostLabel => 'IP地址';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpHostHint => '192.168.40.10';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortLabel => '端口';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpPortHint => '5000';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpStatus_notConnected => '输入目标地址,然后连接';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpStatus_connectingTo(String endpoint) {
|
||||||
|
return '连接到 $endpoint...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorHostRequired => '需要提供IP地址。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorPortInvalid => '端口号必须在 1 到 65535 之间。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorUnsupported => '此平台不支持 TCP 传输。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tcpErrorTimedOut => 'TCP 连接超时。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String tcpConnectionFailed(String error) {
|
||||||
|
return 'TCP 连接失败:$error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenTitle => '通过USB连接';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenSubtitle => '选择已检测到的串行设备,并直接连接到您的 MeshCore 节点。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenStatus => '选择一个 USB 设备';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenNote => 'USB 串行接口在支持的 Android 设备和桌面平台上处于活动状态。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbScreenEmptyState => '未找到任何 USB 设备。请插入一个,然后刷新。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPermissionDenied => '拒绝了USB权限。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorDeviceMissing => '所选的USB设备已不再可用。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorInvalidPort => '选择一个有效的USB设备。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorBusy => '还有一个 USB 连接请求正在进行中。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNotConnected => '没有连接任何USB设备。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorOpenFailed => '未能打开所选的USB设备。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectFailed => '未能连接到所选的USB设备。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorUnsupported => '此平台不支持USB串行通信。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorAlreadyActive => 'USB 连接已建立。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorNoDeviceSelected => '未选择任何 USB 设备。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorPortClosed => 'USB 连接未建立。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbErrorConnectTimedOut => '连接超时。请确保设备已安装 USB 伴侣固件。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbFallbackDeviceName => 'Web 串流设备';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_notConnected => '选择一个 USB 设备';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_connecting => '连接USB设备...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get usbStatus_searching => '正在搜索 USB 设备...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String usbConnectionFailed(String error) {
|
||||||
|
return 'USB 连接失败:$error';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_scanning => '正在搜索设备...';
|
String get scanner_scanning => '正在搜索设备...';
|
||||||
|
|
||||||
@@ -148,6 +268,13 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get scanner_bluetoothOffMessage => '请开启蓝牙以搜索设备';
|
String get scanner_bluetoothOffMessage => '请开启蓝牙以搜索设备';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequired => '需要 Chrome 浏览器';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_chromeRequiredMessage =>
|
||||||
|
'此 Web 应用程序需要 Google Chrome 或基于 Chromium 的浏览器以支持蓝牙。';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanner_enableBluetooth => '启用蓝牙';
|
String get scanner_enableBluetooth => '启用蓝牙';
|
||||||
|
|
||||||
@@ -226,6 +353,12 @@ class AppLocalizationsZh 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 => '隐私模式';
|
||||||
|
|
||||||
@@ -241,6 +374,47 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_privacyModeDisabled => '隐私模式已关闭';
|
String get settings_privacyModeDisabled => '隐私模式已关闭';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacy => '隐私设置';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySubtitle => '控制要共享的信息。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_privacySettingsDescription => '选择您的设备与他人共享的信息。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_denyAll => '拒绝所有';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowByContact => '按联系人标志允许';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_allowAll => '允许全部';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryBaseMode => '遥测基础模式';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryLocationMode => '遥测位置模式';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryEnvironmentMode => '遥测环境模式';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocation => '广告位置';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_advertLocationSubtitle => '在广告中包含位置';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String settings_multiAck(String value) {
|
||||||
|
return '多重ACK:$value';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_telemetryModeUpdated => '遥测模式已更新';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_actions => '操作';
|
String get settings_actions => '操作';
|
||||||
|
|
||||||
@@ -515,6 +689,43 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_autoRouteRotationDisabled => '自动路径轮换已禁用';
|
String get appSettings_autoRouteRotationDisabled => '自动路径轮换已禁用';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeight => '最大路径重量';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxRouteWeightSubtitle => '一条路径可以累积的最大重量,取决于成功交付的数量。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeight => '初始路线权重';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_initialRouteWeightSubtitle => '新发现路径的初始重量';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrement => '成功权重增加';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightSuccessIncrementSubtitle =>
|
||||||
|
'在成功交付后,将重量添加到路径中';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrement => '失败权重降低';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_routeWeightFailureDecrementSubtitle =>
|
||||||
|
'从一条路径上移除的货物,由于无法成功交付而移除。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetries => '最大消息重试次数';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_maxMessageRetriesSubtitle => '在将消息标记为失败之前,允许尝试的次数';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String path_routeWeight(String weight, String max) {
|
||||||
|
return '$weight/$max';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_battery => '电池';
|
String get appSettings_battery => '电池';
|
||||||
|
|
||||||
@@ -712,6 +923,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get contacts_groupNameRequired => '请输入群聊名称';
|
String get contacts_groupNameRequired => '请输入群聊名称';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_groupNameReserved => '该群组名称已被保留';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contacts_groupAlreadyExists(String name) {
|
String contacts_groupAlreadyExists(String name) {
|
||||||
return '名为 \"$name\" 的群聊已存在';
|
return '名为 \"$name\" 的群聊已存在';
|
||||||
@@ -750,6 +964,39 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
return '最后在线 $days 天前';
|
return '最后在线 $days 天前';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_info => '联系信息';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_settings => '联系人设置';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_telemetry => '遥测数据';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_lastSeen => '最近出现';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_clearChat => '清除聊天记录';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBase => '遥测基站';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleBaseSubtitle => '允许共享电池电量和基本遥测数据';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLoc => '遥测位置';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleLocSubtitle => '允许共享位置数据';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnv => '遥测环境';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contact_teleEnvSubtitle => '允许共享环境传感器数据';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_title => '频道';
|
String get channels_title => '频道';
|
||||||
|
|
||||||
@@ -1288,6 +1535,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_shareMarkerHere => '在此分享标记';
|
String get map_shareMarkerHere => '在此分享标记';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_setAsMyLocation => '设置为我的位置';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_pinLabel => '标签';
|
String get map_pinLabel => '标签';
|
||||||
|
|
||||||
@@ -1332,6 +1582,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_otherNodes => '其他节点';
|
String get map_otherNodes => '其他节点';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showOverlaps => '重复键重叠';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_keyPrefix => '关键字前缀';
|
String get map_keyPrefix => '关键字前缀';
|
||||||
|
|
||||||
@@ -1347,6 +1600,15 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_showSharedMarkers => '显示共享标记';
|
String get map_showSharedMarkers => '显示共享标记';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showGuessedLocations => '显示猜测的节点位置';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_showDiscoveryContacts => '显示发现联系人';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_guessedLocation => '猜测的位置';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_lastSeenTime => '最后在线时间';
|
String get map_lastSeenTime => '最后在线时间';
|
||||||
|
|
||||||
@@ -1365,6 +1627,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_runTrace => '运行路径追踪';
|
String get map_runTrace => '运行路径追踪';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_runTraceWithReturnPath => '沿着相同的路径返回';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_removeLast => '移除最后一个';
|
String get map_removeLast => '移除最后一个';
|
||||||
|
|
||||||
@@ -2890,4 +3155,71 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get snrIndicator_lastSeen => '最近访问';
|
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 => '您确定要删除所有发现的联系人吗?';
|
||||||
}
|
}
|
||||||
|
|||||||
+145
-1
@@ -285,6 +285,7 @@
|
|||||||
"contacts_newGroup": "Nieuwe Groep",
|
"contacts_newGroup": "Nieuwe Groep",
|
||||||
"contacts_groupName": "Groepnaam",
|
"contacts_groupName": "Groepnaam",
|
||||||
"contacts_groupNameRequired": "De groepnaam is verplicht.",
|
"contacts_groupNameRequired": "De groepnaam is verplicht.",
|
||||||
|
"contacts_groupNameReserved": "Deze groepsnaam is gereserveerd",
|
||||||
"contacts_groupAlreadyExists": "De groep \"{name}\" bestaat al.",
|
"contacts_groupAlreadyExists": "De groep \"{name}\" bestaat al.",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1605,6 +1606,8 @@
|
|||||||
"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_lastSeen": "Laatst gezien",
|
||||||
"snrIndicator_nearByRepeaters": "Nabije herhalingseenheden",
|
"snrIndicator_nearByRepeaters": "Nabije herhalingseenheden",
|
||||||
@@ -1799,5 +1802,146 @@
|
|||||||
"contacts_searchContactsNoNumber": "Zoek contacten...",
|
"contacts_searchContactsNoNumber": "Zoek contacten...",
|
||||||
"contacts_searchUsers": "Zoek {number}{str} gebruikers...",
|
"contacts_searchUsers": "Zoek {number}{str} gebruikers...",
|
||||||
"contacts_searchFavorites": "Zoek {number}{str} favorieten...",
|
"contacts_searchFavorites": "Zoek {number}{str} favorieten...",
|
||||||
"contacts_searchRoomServers": "Zoek {number}{str} Room servers..."
|
"contacts_searchRoomServers": "Zoek {number}{str} Room servers...",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Gebruikers automatisch toevoegen",
|
||||||
|
"contactsSettings_title": "Instellingen voor contacten",
|
||||||
|
"settings_contactSettings": "Contactinstellingen",
|
||||||
|
"contactsSettings_otherTitle": "Andere instellingen voor contactgerelateerde zaken",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Sta toe dat de companion automatisch ontdekte repeaters toevoegt",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Automatisch kamerservers toevoegen",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Sta toe dat de companion automatisch ontdekte kamer servers toevoegt.",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Automatisch sensoren toevoegen",
|
||||||
|
"settings_contactSettingsSubtitle": "Instellingen voor het toevoegen van contacten",
|
||||||
|
"contactsSettings_autoAddTitle": "Automatische detectie",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Sta toe dat de companion automatisch ontdekte sensoren toevoegt",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Sta toe dat de companion automatisch ontdekte gebruikers toevoegt",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Automatisch herhalingstoestellen toevoegen",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Overschrijf Oudste",
|
||||||
|
"discoveredContacts_noMatching": "Geen overeenkomende contacten",
|
||||||
|
"discoveredContacts_addContact": "Contact toevoegen",
|
||||||
|
"discoveredContacts_copyContact": "Kopieer contact naar klembord",
|
||||||
|
"discoveredContacts_deleteContact": "Contact verwijderen",
|
||||||
|
"discoveredContacts_Title": "Ontdekte contacten",
|
||||||
|
"discoveredContacts_contactAdded": "Contact toegevoegd",
|
||||||
|
"discoveredContacts_searchHint": "Ontdekte contacten zoeken",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Wanneer de contactenlijst vol is, wordt de oudste niet-favoriete contactpersoon vervangen.",
|
||||||
|
"common_deleteAll": "Alles verwijderen",
|
||||||
|
"discoveredContacts_deleteContactAll": "Verwijder alle ontdekte contacten",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Weet u zeker dat u alle ontdekte contacten wilt verwijderen?",
|
||||||
|
"map_guessedLocation": "Geroerde locatie",
|
||||||
|
"map_showGuessedLocations": "Toon de voorspelde locaties van de knopen",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenSubtitle": "Selecteer een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenTitle": "Verbind via USB",
|
||||||
|
"usbScreenStatus": "Selecteer een USB-apparaat",
|
||||||
|
"usbScreenNote": "USB-serieel is actief op ondersteunde Android-apparaten en desktop-platforms.",
|
||||||
|
"usbScreenEmptyState": "Geen USB-apparaten gevonden. Sluit er een aan en herlaad.",
|
||||||
|
"usbErrorPermissionDenied": "Toegang via USB is geweigerd.",
|
||||||
|
"usbErrorDeviceMissing": "Het geselecteerde USB-apparaat is niet meer beschikbaar.",
|
||||||
|
"usbErrorInvalidPort": "Selecteer een geldig USB-apparaat.",
|
||||||
|
"usbErrorBusy": "Een andere verzoek om een USB-verbinding is al in behandeling.",
|
||||||
|
"usbErrorNotConnected": "Er is geen USB-apparaat aangesloten.",
|
||||||
|
"usbErrorOpenFailed": "Kon het geselecteerde USB-apparaat niet openen.",
|
||||||
|
"usbErrorConnectFailed": "Kon niet verbinding maken met het geselecteerde USB-apparaat.",
|
||||||
|
"usbErrorUnsupported": "USB-serieel is niet ondersteund op deze platform.",
|
||||||
|
"usbErrorAlreadyActive": "Een USB-verbinding is al actief.",
|
||||||
|
"usbErrorNoDeviceSelected": "Geen USB-apparaat is geselecteerd.",
|
||||||
|
"usbErrorPortClosed": "De USB-verbinding is niet actief.",
|
||||||
|
"usbFallbackDeviceName": "Web-serieapparaat",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbConnectionFailed": "Fout bij de USB-verbinding: {error}",
|
||||||
|
"usbStatus_notConnected": "Selecteer een USB-apparaat",
|
||||||
|
"usbStatus_connecting": "Verbinding maken met USB-apparaat...",
|
||||||
|
"usbStatus_searching": "Zoeken naar USB-apparaten...",
|
||||||
|
"usbErrorConnectTimedOut": "Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft.",
|
||||||
|
"@tcpStatus_connectingTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"endpoint": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@tcpConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tcpScreenTitle": "Verbind via TCP",
|
||||||
|
"tcpHostLabel": "IP-adres",
|
||||||
|
"tcpHostHint": "192.168.40.10",
|
||||||
|
"connectionChoiceTcpLabel": "TCP",
|
||||||
|
"tcpPortLabel": "Poort",
|
||||||
|
"tcpPortHint": "5000",
|
||||||
|
"tcpStatus_notConnected": "Voer het eindpunt in en verbind",
|
||||||
|
"tcpStatus_connectingTo": "Verbinding maken met {endpoint}...",
|
||||||
|
"tcpErrorHostRequired": "Een IP-adres is vereist.",
|
||||||
|
"tcpErrorPortInvalid": "De poortwaarde moet tussen 1 en 65535 liggen.",
|
||||||
|
"tcpErrorUnsupported": "TCP-transport wordt niet ondersteund op deze platform.",
|
||||||
|
"tcpErrorTimedOut": "De TCP-verbinding is verlopen.",
|
||||||
|
"tcpConnectionFailed": "Verbinding met TCP mislukt: {error}",
|
||||||
|
"map_showDiscoveryContacts": "Ontdek contacten weergeven",
|
||||||
|
"map_setAsMyLocation": "Stel dit in als mijn locatie",
|
||||||
|
"@path_routeWeight": {
|
||||||
|
"placeholders": {
|
||||||
|
"weight": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"max": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings_privacy": "Privacyinstellingen",
|
||||||
|
"settings_privacySubtitle": "Beheer welke informatie wordt gedeeld",
|
||||||
|
"settings_telemetryLocationMode": "Telemetrie-locatiemodus",
|
||||||
|
"settings_telemetryEnvironmentMode": "Telemetrie-omgevingsmodus",
|
||||||
|
"settings_advertLocation": "Advertentielocatie",
|
||||||
|
"settings_advertLocationSubtitle": "Locatie opnemen in advertentie",
|
||||||
|
"settings_privacySettingsDescription": "Kies welke informatie uw apparaat deelt met anderen",
|
||||||
|
"settings_allowByContact": "Toestaan op basis van contactvlaggen",
|
||||||
|
"settings_allowAll": "Alles toestaan",
|
||||||
|
"settings_denyAll": "Weiger alles",
|
||||||
|
"contact_info": "Contactinformatie",
|
||||||
|
"settings_telemetryBaseMode": "Telemetrie-basismodus",
|
||||||
|
"contact_teleBase": "Telemetrie_basis",
|
||||||
|
"contact_teleLoc": "Telemetrielocatie",
|
||||||
|
"contact_teleLocSubtitle": "Locatiegegevens delen toestaan",
|
||||||
|
"contact_teleEnv": "Telemetrieomgeving",
|
||||||
|
"contact_teleEnvSubtitle": "Delen van omgevingsensordata toestaan",
|
||||||
|
"contact_settings": "Contactinstellingen",
|
||||||
|
"contact_telemetry": "Telemetrie",
|
||||||
|
"contact_lastSeen": "Laatst gezien",
|
||||||
|
"contact_clearChat": "Chat leegmaken",
|
||||||
|
"contact_teleBaseSubtitle": "Sta delen van batterij niveau en basis telemetrie toe",
|
||||||
|
"@settings_multiAck": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"appSettings_maxRouteWeightSubtitle": "Het maximale gewicht dat een route kan bereiken door succesvolle leveringen.",
|
||||||
|
"appSettings_initialRouteWeight": "เริ่มต้น gewicht van de route",
|
||||||
|
"appSettings_maxRouteWeight": "Maximale gewicht voor de route",
|
||||||
|
"appSettings_initialRouteWeightSubtitle": "Startgewicht voor nieuwe, ontdekte routes",
|
||||||
|
"appSettings_routeWeightSuccessIncrement": "Toename in het gewicht van het succes",
|
||||||
|
"appSettings_routeWeightSuccessIncrementSubtitle": "Gewicht wordt toegevoegd aan een route na een succesvolle levering.",
|
||||||
|
"appSettings_routeWeightFailureDecrement": "Vermindering van het gewicht van fouten",
|
||||||
|
"appSettings_routeWeightFailureDecrementSubtitle": "Gewicht verwijderd van een pad na een mislukte levering",
|
||||||
|
"appSettings_maxMessageRetries": "Aantal pogingen om berichten te versturen",
|
||||||
|
"appSettings_maxMessageRetriesSubtitle": "Aantal pogingen om een bericht opnieuw te versturen voordat het als mislukt wordt gemarkeerd",
|
||||||
|
"path_routeWeight": "{weight}/{max}",
|
||||||
|
"settings_telemetryModeUpdated": "Telemetrie-modus bijgewerkt",
|
||||||
|
"settings_multiAck": "Multi-ACKs: {value}",
|
||||||
|
"map_showOverlaps": "Herhalingssleutel overlapt",
|
||||||
|
"map_runTraceWithReturnPath": "Terugkeren op hetzelfde pad."
|
||||||
}
|
}
|
||||||
+397
-215
File diff suppressed because it is too large
Load Diff
+145
-1
@@ -285,6 +285,7 @@
|
|||||||
"contacts_newGroup": "Novo Grupo",
|
"contacts_newGroup": "Novo Grupo",
|
||||||
"contacts_groupName": "Nome do grupo",
|
"contacts_groupName": "Nome do grupo",
|
||||||
"contacts_groupNameRequired": "O nome do grupo é obrigatório.",
|
"contacts_groupNameRequired": "O nome do grupo é obrigatório.",
|
||||||
|
"contacts_groupNameReserved": "Este nome de grupo está reservado",
|
||||||
"contacts_groupAlreadyExists": "O grupo \"{name}\" já existe",
|
"contacts_groupAlreadyExists": "O grupo \"{name}\" já existe",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1606,6 +1607,8 @@
|
|||||||
"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_nearByRepeaters": "Repetidores Próximos",
|
||||||
"snrIndicator_lastSeen": "Visto pela última vez",
|
"snrIndicator_lastSeen": "Visto pela última vez",
|
||||||
"chat_ShowAllPaths": "Mostrar todos os caminhos",
|
"chat_ShowAllPaths": "Mostrar todos os caminhos",
|
||||||
@@ -1799,5 +1802,146 @@
|
|||||||
"contacts_searchUsers": "Pesquisar {number}{str} Usuários...",
|
"contacts_searchUsers": "Pesquisar {number}{str} Usuários...",
|
||||||
"contacts_searchContactsNoNumber": "Pesquisar Contatos...",
|
"contacts_searchContactsNoNumber": "Pesquisar Contatos...",
|
||||||
"contacts_unread": "Não lido",
|
"contacts_unread": "Não lido",
|
||||||
"contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala..."
|
"contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala...",
|
||||||
|
"settings_contactSettings": "Configurações de Contato",
|
||||||
|
"contactsSettings_otherTitle": "Outras configurações relacionadas a contatos",
|
||||||
|
"contactsSettings_title": "Configurações de contatos",
|
||||||
|
"contactsSettings_autoAddTitle": "Descoberta Automática",
|
||||||
|
"settings_contactSettingsSubtitle": "Configurações para como os contatos são adicionados",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Adicionar usuários automaticamente",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Permitir que o companheiro adicione automaticamente os repetidores descobertos.",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Adicionar automaticamente servidores de sala",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Sobrescrever o Mais Antigo",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Adicionar sensores automaticamente",
|
||||||
|
"discoveredContacts_Title": "Contatos Descobertos",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Permitir que o companheiro adicione automaticamente os usuários descobertos.",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Adicionar repetidores automaticamente",
|
||||||
|
"discoveredContacts_noMatching": "Nenhum contato correspondente",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Permitir que o companheiro adicione automaticamente os servidores de salas descobertos.",
|
||||||
|
"discoveredContacts_searchHint": "Pesquisar contatos descobertos",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Permitir que o companheiro adicione automaticamente sensores descobertos.",
|
||||||
|
"discoveredContacts_copyContact": "Copiar Contato para a área de transferência",
|
||||||
|
"discoveredContacts_deleteContact": "Excluir Contato",
|
||||||
|
"discoveredContacts_contactAdded": "Contato adicionado",
|
||||||
|
"discoveredContacts_addContact": "Adicionar Contato",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Quando a lista de contatos estiver cheia, o contato mais antigo não favoritado será substituído.",
|
||||||
|
"common_deleteAll": "Excluir Tudo",
|
||||||
|
"discoveredContacts_deleteContactAll": "Excluir Todos os Contatos Descobertos",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Tem certeza de que deseja excluir todos os contatos descobertos?",
|
||||||
|
"map_guessedLocation": "Localização estimada",
|
||||||
|
"map_showGuessedLocations": "Mostrar as localizações dos nós estimados",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenTitle": "Conecte via USB",
|
||||||
|
"usbScreenSubtitle": "Selecione um dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.",
|
||||||
|
"usbScreenStatus": "Selecione um dispositivo USB",
|
||||||
|
"usbScreenNote": "A comunicação serial via USB está ativa em dispositivos Android e plataformas de desktop compatíveis.",
|
||||||
|
"usbScreenEmptyState": "Nenhum dispositivo USB encontrado. Conecte um e atualize.",
|
||||||
|
"usbErrorPermissionDenied": "A permissão para acesso via USB foi negada.",
|
||||||
|
"usbErrorDeviceMissing": "O dispositivo USB selecionado não está mais disponível.",
|
||||||
|
"usbErrorInvalidPort": "Selecione um dispositivo USB válido.",
|
||||||
|
"usbErrorBusy": "Já existe uma solicitação de conexão USB em andamento.",
|
||||||
|
"usbErrorNotConnected": "Não há nenhum dispositivo USB conectado.",
|
||||||
|
"usbErrorOpenFailed": "Não foi possível abrir o dispositivo USB selecionado.",
|
||||||
|
"usbErrorConnectFailed": "Não foi possível conectar ao dispositivo USB selecionado.",
|
||||||
|
"usbErrorUnsupported": "A comunicação serial via USB não é suportada nesta plataforma.",
|
||||||
|
"usbErrorAlreadyActive": "A conexão USB já está ativa.",
|
||||||
|
"usbErrorNoDeviceSelected": "Nenhum dispositivo USB foi selecionado.",
|
||||||
|
"usbErrorPortClosed": "A conexão USB não está ativa.",
|
||||||
|
"usbFallbackDeviceName": "Dispositivo de Serial para a Web",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_searching": "Procurando por dispositivos USB...",
|
||||||
|
"usbStatus_notConnected": "Selecione um dispositivo USB",
|
||||||
|
"usbConnectionFailed": "Falha na conexão USB: {error}",
|
||||||
|
"usbStatus_connecting": "Conectando ao dispositivo USB...",
|
||||||
|
"usbErrorConnectTimedOut": "A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion.",
|
||||||
|
"@tcpStatus_connectingTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"endpoint": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@tcpConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tcpHostLabel": "Endereço IP",
|
||||||
|
"connectionChoiceTcpLabel": "TCP",
|
||||||
|
"tcpScreenTitle": "Estabelecer conexão via TCP",
|
||||||
|
"tcpHostHint": "192.168.40.10",
|
||||||
|
"tcpPortLabel": "Porta",
|
||||||
|
"tcpPortHint": "5000",
|
||||||
|
"tcpStatus_notConnected": "Insira o endereço final e conecte-se.",
|
||||||
|
"tcpStatus_connectingTo": "Conectando a {endpoint}...",
|
||||||
|
"tcpErrorHostRequired": "É necessário fornecer um endereço IP.",
|
||||||
|
"tcpErrorPortInvalid": "O valor do porto deve estar entre 1 e 65535.",
|
||||||
|
"tcpErrorUnsupported": "O protocolo TCP não é suportado nesta plataforma.",
|
||||||
|
"tcpErrorTimedOut": "A conexão TCP expirou.",
|
||||||
|
"tcpConnectionFailed": "Falha na conexão TCP: {error}",
|
||||||
|
"map_showDiscoveryContacts": "Mostrar Contatos de Descoberta",
|
||||||
|
"map_setAsMyLocation": "Defina minha localização",
|
||||||
|
"@path_routeWeight": {
|
||||||
|
"placeholders": {
|
||||||
|
"weight": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"max": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings_privacySettingsDescription": "Escolha quais informações o seu dispositivo compartilha com os outros.",
|
||||||
|
"settings_allowByContact": "Permitir por bandeiras de contato",
|
||||||
|
"settings_telemetryLocationMode": "Modo de Localização de Telemetria",
|
||||||
|
"settings_telemetryEnvironmentMode": "Modo de Ambiente de Telemetria",
|
||||||
|
"settings_advertLocation": "Localização do Anúncio",
|
||||||
|
"settings_advertLocationSubtitle": "Incluir localização no anúncio",
|
||||||
|
"settings_privacySubtitle": "Controle o que é compartilhado.",
|
||||||
|
"settings_denyAll": "Negar todos",
|
||||||
|
"settings_allowAll": "Permitir todos",
|
||||||
|
"settings_privacy": "Configurações de Privacidade",
|
||||||
|
"contact_info": "Informações de Contato",
|
||||||
|
"settings_telemetryBaseMode": "Modo Base de Telemetria",
|
||||||
|
"contact_teleBase": "Base de Telemetria",
|
||||||
|
"contact_teleLoc": "Localização de Telemetria",
|
||||||
|
"contact_teleLocSubtitle": "Permitir compartilhamento de dados de localização",
|
||||||
|
"contact_teleEnv": "Ambiente de Telemetria",
|
||||||
|
"contact_teleEnvSubtitle": "Permitir compartilhamento de dados do sensor de ambiente",
|
||||||
|
"contact_lastSeen": "Visto pela última vez",
|
||||||
|
"contact_clearChat": "Limpar Chat",
|
||||||
|
"contact_telemetry": "Telemetria",
|
||||||
|
"contact_settings": "Configurações de Contato",
|
||||||
|
"contact_teleBaseSubtitle": "Permitir compartilhamento do nível da bateria e telemetria básica",
|
||||||
|
"@settings_multiAck": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"appSettings_initialRouteWeight": "Peso Inicial da Rota",
|
||||||
|
"appSettings_maxRouteWeight": "Peso Máximo da Rota",
|
||||||
|
"appSettings_maxRouteWeightSubtitle": "Peso máximo que um determinado percurso pode acumular com entregas bem-sucedidas.",
|
||||||
|
"appSettings_initialRouteWeightSubtitle": "Peso inicial para novos caminhos descobertos",
|
||||||
|
"appSettings_routeWeightSuccessIncrement": "Aumento do peso para indicar sucesso",
|
||||||
|
"appSettings_routeWeightSuccessIncrementSubtitle": "Peso adicionado a um caminho após a entrega bem-sucedida.",
|
||||||
|
"appSettings_routeWeightFailureDecrement": "Redução do peso da falha",
|
||||||
|
"appSettings_routeWeightFailureDecrementSubtitle": "Peso removido de um caminho após uma tentativa de entrega malsucedida.",
|
||||||
|
"appSettings_maxMessageRetries": "Número máximo de tentativas de envio de mensagens",
|
||||||
|
"appSettings_maxMessageRetriesSubtitle": "Número de tentativas de reenvio antes de classificar uma mensagem como falha.",
|
||||||
|
"path_routeWeight": "{weight}/{max}",
|
||||||
|
"settings_telemetryModeUpdated": "Modo de telemetria atualizado",
|
||||||
|
"settings_multiAck": "Multi-ACKs: {value}",
|
||||||
|
"map_showOverlaps": "Sobreposições da Chave Repeater",
|
||||||
|
"map_runTraceWithReturnPath": "Retornar ao mesmo caminho."
|
||||||
}
|
}
|
||||||
+145
-1
@@ -212,6 +212,7 @@
|
|||||||
"contacts_newGroup": "Новая группа",
|
"contacts_newGroup": "Новая группа",
|
||||||
"contacts_groupName": "Имя группы",
|
"contacts_groupName": "Имя группы",
|
||||||
"contacts_groupNameRequired": "Имя группы обязательно",
|
"contacts_groupNameRequired": "Имя группы обязательно",
|
||||||
|
"contacts_groupNameReserved": "Это имя группы зарезервировано",
|
||||||
"contacts_groupAlreadyExists": "Группа \"{name}\" уже существует",
|
"contacts_groupAlreadyExists": "Группа \"{name}\" уже существует",
|
||||||
"contacts_filterContacts": "Фильтр контактов...",
|
"contacts_filterContacts": "Фильтр контактов...",
|
||||||
"contacts_noContactsMatchFilter": "Нет контактов, соответствующих фильтру",
|
"contacts_noContactsMatchFilter": "Нет контактов, соответствующих фильтру",
|
||||||
@@ -846,6 +847,8 @@
|
|||||||
"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_nearByRepeaters": "Ближайшие ретрансляторы",
|
||||||
"snrIndicator_lastSeen": "Последний раз видели",
|
"snrIndicator_lastSeen": "Последний раз видели",
|
||||||
"chat_ShowAllPaths": "Показать все пути",
|
"chat_ShowAllPaths": "Показать все пути",
|
||||||
@@ -1039,5 +1042,146 @@
|
|||||||
"contacts_unread": "Непрочитанное",
|
"contacts_unread": "Непрочитанное",
|
||||||
"contacts_searchRoomServers": "Поиск {number}{str} серверов комнат...",
|
"contacts_searchRoomServers": "Поиск {number}{str} серверов комнат...",
|
||||||
"contacts_searchFavorites": "Поиск {number}{str} избранного...",
|
"contacts_searchFavorites": "Поиск {number}{str} избранного...",
|
||||||
"contacts_searchUsers": "Поиск {number}{str} пользователей..."
|
"contacts_searchUsers": "Поиск {number}{str} пользователей...",
|
||||||
|
"settings_contactSettings": "Настройки контактов",
|
||||||
|
"settings_contactSettingsSubtitle": "Настройки добавления контактов",
|
||||||
|
"contactsSettings_autoAddTitle": "Автоматическое обнаружение",
|
||||||
|
"contactsSettings_title": "Настройки контактов",
|
||||||
|
"contactsSettings_otherTitle": "Другие настройки, связанные с контактами",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Разрешить компаньону автоматически добавлять обнаруженных пользователей",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Автоматически добавлять серверы комнат",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Автоматически добавлять датчики",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Разрешить компаньону автоматически добавлять обнаруженные датчики",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Автоматически добавлять пользователей",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Перезаписать самое старое",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Автоматически добавлять ретрансляторы",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Разрешить спутнику автоматически добавлять обнаруженные ретрансляторы",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Разрешить компаньону автоматически добавлять обнаруженные сервера комнат.",
|
||||||
|
"discoveredContacts_noMatching": "Нет совпадающих контактов",
|
||||||
|
"discoveredContacts_searchHint": "Найденные контакты поиска",
|
||||||
|
"discoveredContacts_contactAdded": "Контакт добавлен",
|
||||||
|
"discoveredContacts_copyContact": "Копировать контакт в буфер обмена",
|
||||||
|
"discoveredContacts_addContact": "Добавить контакт",
|
||||||
|
"discoveredContacts_Title": "Обнаруженные контакты",
|
||||||
|
"discoveredContacts_deleteContact": "Удалить контакт",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Когда список контактов заполнен, будет заменен самый старый контакт, который не находится в избранном.",
|
||||||
|
"common_deleteAll": "Удалить все",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Вы уверены, что хотите удалить все обнаруженные контакты?",
|
||||||
|
"discoveredContacts_deleteContactAll": "Удалить Все Обнаруженные Контакты",
|
||||||
|
"map_guessedLocation": "Угаданное место",
|
||||||
|
"map_showGuessedLocations": "Отобразить предполагаемые места расположения узлов",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenSubtitle": "Выберите обнаруженное устройство с последовательным интерфейсом и подключите его напрямую к вашему узлу MeshCore.",
|
||||||
|
"usbScreenTitle": "Подключение через USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenStatus": "Выберите USB-устройство",
|
||||||
|
"usbScreenNote": "USB-серийный порт активен на поддерживаемых устройствах Android и на настольных платформах.",
|
||||||
|
"usbScreenEmptyState": "Не обнаружено устройств USB. Подключите одно из них и обновите список.",
|
||||||
|
"usbErrorPermissionDenied": "Запрос на доступ через USB был отклонен.",
|
||||||
|
"usbErrorDeviceMissing": "Выбранное USB-устройство больше недоступно.",
|
||||||
|
"usbErrorInvalidPort": "Выберите действительное USB-устройство.",
|
||||||
|
"usbErrorBusy": "Еще одно запрошенное соединение через USB уже находится в процессе.",
|
||||||
|
"usbErrorNotConnected": "Ни одно USB-устройство не подключено.",
|
||||||
|
"usbErrorOpenFailed": "Не удалось открыть выбранное USB-устройство.",
|
||||||
|
"usbErrorConnectFailed": "Не удалось установить соединение с выбранным USB-устройством.",
|
||||||
|
"usbErrorUnsupported": "Поддержка последовательного USB отсутствует на данной платформе.",
|
||||||
|
"usbErrorAlreadyActive": "USB-соединение уже установлено.",
|
||||||
|
"usbErrorNoDeviceSelected": "Не было выбрано ни одно устройство USB.",
|
||||||
|
"usbErrorPortClosed": "USB-соединение не установлено.",
|
||||||
|
"usbFallbackDeviceName": "Устройство для последовательного подключения к сети",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_searching": "Поиск USB-устройств...",
|
||||||
|
"usbStatus_connecting": "Подключение к USB-устройству...",
|
||||||
|
"usbConnectionFailed": "Не удалось установить соединение через USB: {error}",
|
||||||
|
"usbStatus_notConnected": "Выберите USB-устройство",
|
||||||
|
"usbErrorConnectTimedOut": "Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion.",
|
||||||
|
"@tcpStatus_connectingTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"endpoint": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@tcpConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tcpHostHint": "192.168.40.10",
|
||||||
|
"connectionChoiceTcpLabel": "TCP",
|
||||||
|
"tcpHostLabel": "IP-адрес",
|
||||||
|
"tcpScreenTitle": "Установить соединение по протоколу TCP",
|
||||||
|
"tcpPortLabel": "Порт",
|
||||||
|
"tcpPortHint": "5000",
|
||||||
|
"tcpStatus_notConnected": "Введите адрес и подключитесь.",
|
||||||
|
"tcpStatus_connectingTo": "Подключение к {endpoint}...",
|
||||||
|
"tcpErrorHostRequired": "Необходимо указать IP-адрес.",
|
||||||
|
"tcpErrorPortInvalid": "Порт должен находиться в диапазоне от 1 до 65535.",
|
||||||
|
"tcpErrorUnsupported": "Протокол TCP не поддерживается на этой платформе.",
|
||||||
|
"tcpErrorTimedOut": "Соединение TCP не удалось установить.",
|
||||||
|
"tcpConnectionFailed": "Не удалось установить соединение TCP: {error}",
|
||||||
|
"map_showDiscoveryContacts": "Показать контакты Discovery",
|
||||||
|
"map_setAsMyLocation": "Установить мое местоположение",
|
||||||
|
"@path_routeWeight": {
|
||||||
|
"placeholders": {
|
||||||
|
"weight": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"max": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings_privacy": "Настройки конфиденциальности",
|
||||||
|
"settings_privacySubtitle": "Контролируйте, какую информацию делиться.",
|
||||||
|
"settings_telemetryLocationMode": "Режим местоположения телеметрии",
|
||||||
|
"settings_telemetryEnvironmentMode": "Режим среды телеметрии",
|
||||||
|
"settings_advertLocation": "Местоположение рекламы",
|
||||||
|
"settings_advertLocationSubtitle": "Включить местоположение в объявление",
|
||||||
|
"settings_allowAll": "Разрешить все",
|
||||||
|
"settings_privacySettingsDescription": "Выберите, какую информацию ваше устройство будет делиться с другими.",
|
||||||
|
"settings_denyAll": "Отклонить все",
|
||||||
|
"settings_allowByContact": "Разрешить по флагам контактов",
|
||||||
|
"contact_info": "Контактная информация",
|
||||||
|
"settings_telemetryBaseMode": "Базовый режим телеметрии",
|
||||||
|
"contact_teleBase": "База телеметрии",
|
||||||
|
"contact_teleLoc": "Местоположение телеметрии",
|
||||||
|
"contact_teleLocSubtitle": "Разрешить обмен данными о местоположении",
|
||||||
|
"contact_teleEnv": "Среда телеметрии",
|
||||||
|
"contact_teleEnvSubtitle": "Разрешить обмен данными датчиков окружающей среды",
|
||||||
|
"contact_settings": "Настройки контактов",
|
||||||
|
"contact_telemetry": "Телеметрия",
|
||||||
|
"contact_clearChat": "Очистить чат",
|
||||||
|
"contact_lastSeen": "Последний раз видели",
|
||||||
|
"contact_teleBaseSubtitle": "Разрешить обмен уровнем заряда батареи и базовой телеметрией",
|
||||||
|
"@settings_multiAck": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"appSettings_maxRouteWeight": "Максимальный допустимый вес маршрута",
|
||||||
|
"appSettings_maxRouteWeightSubtitle": "Максимальный вес, который может быть перевезён по определённому маршруту при успешных доставках.",
|
||||||
|
"appSettings_initialRouteWeightSubtitle": "Начальный вес для новых, только что открытых маршрутов",
|
||||||
|
"appSettings_initialRouteWeight": "Начальный вес маршрута",
|
||||||
|
"appSettings_routeWeightSuccessIncrement": "Увеличение веса успеха",
|
||||||
|
"appSettings_routeWeightSuccessIncrementSubtitle": "Вес, добавленный к маршруту после успешной доставки.",
|
||||||
|
"appSettings_routeWeightFailureDecrement": "Уменьшение веса неудачи",
|
||||||
|
"appSettings_routeWeightFailureDecrementSubtitle": "Вес, который был удален с пути после неудачной доставки.",
|
||||||
|
"appSettings_maxMessageRetries": "Максимальное количество повторных попыток отправки сообщения",
|
||||||
|
"appSettings_maxMessageRetriesSubtitle": "Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.",
|
||||||
|
"path_routeWeight": "{weight}/{max}",
|
||||||
|
"settings_telemetryModeUpdated": "Режим телеметрии обновлен",
|
||||||
|
"settings_multiAck": "Мульти-ACK: {value}",
|
||||||
|
"map_showOverlaps": "Перекрытия ключа повтора",
|
||||||
|
"map_runTraceWithReturnPath": "Вернуться обратно по тому же пути"
|
||||||
}
|
}
|
||||||
+145
-1
@@ -285,6 +285,7 @@
|
|||||||
"contacts_newGroup": "Nová skupina",
|
"contacts_newGroup": "Nová skupina",
|
||||||
"contacts_groupName": "Názov skupiny",
|
"contacts_groupName": "Názov skupiny",
|
||||||
"contacts_groupNameRequired": "Skupina musí mať názov.",
|
"contacts_groupNameRequired": "Skupina musí mať názov.",
|
||||||
|
"contacts_groupNameReserved": "Tento názov skupiny je rezervovaný",
|
||||||
"contacts_groupAlreadyExists": "Skupina \"{name}\" už existuje",
|
"contacts_groupAlreadyExists": "Skupina \"{name}\" už existuje",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1604,6 +1605,8 @@
|
|||||||
"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_lastSeen": "Naposledy videný",
|
||||||
@@ -1799,5 +1802,146 @@
|
|||||||
"contacts_searchRepeaters": "Hľadať {number}{str} opakovače...",
|
"contacts_searchRepeaters": "Hľadať {number}{str} opakovače...",
|
||||||
"contacts_searchUsers": "Hľadať {number}{str} používateľov...",
|
"contacts_searchUsers": "Hľadať {number}{str} používateľov...",
|
||||||
"contacts_searchContactsNoNumber": "Hľadať kontakty...",
|
"contacts_searchContactsNoNumber": "Hľadať kontakty...",
|
||||||
"contacts_unread": "Neprečítané"
|
"contacts_unread": "Neprečítané",
|
||||||
|
"settings_contactSettingsSubtitle": "Nastavenia pre pridávanie kontaktov.",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Automaticky pridávať užívateľov",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Povoliť spoločníkovi automaticky pridávať objavených užívateľov.",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Automaticky pridávať opakovače",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Automaticky pridávať server miestnosti",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Povoliť spoločníkovi automaticky pridať objavené serverové miestnosti.",
|
||||||
|
"contactsSettings_autoAddTitle": "Automatické zisťovanie",
|
||||||
|
"contactsSettings_title": "Nastavenia kontaktov",
|
||||||
|
"contactsSettings_otherTitle": "Ďalšie nastavenia súvisiace s kontaktami",
|
||||||
|
"settings_contactSettings": "Nastavenia kontaktov",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Automaticky pridávať senzory",
|
||||||
|
"discoveredContacts_noMatching": "Žiadne zhodné kontakty",
|
||||||
|
"discoveredContacts_searchHint": "Vyhľadať objavené kontakty",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Povoliť spoločníkovi automaticky pridávať objavené repeater.",
|
||||||
|
"discoveredContacts_contactAdded": "Kontakt bol pridaný",
|
||||||
|
"discoveredContacts_copyContact": "Kopírovať kontakt do schránky",
|
||||||
|
"discoveredContacts_deleteContact": "Zmazať kontakt",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Povoliť spoločníkovi automaticky pridávať objavené senzory.",
|
||||||
|
"discoveredContacts_Title": "Objavené kontakty",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Prepísať najstaršie",
|
||||||
|
"discoveredContacts_addContact": "Pridať kontakt",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Keď je zoznam kontaktov plný, bude nahradený najstarší neoznačený kontakt.",
|
||||||
|
"discoveredContacts_deleteContactAll": "Zmazať všetky objavené kontakty",
|
||||||
|
"common_deleteAll": "Zmazať všetko",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Ste si istí, že chcete zmazať všetky objavené kontakty?",
|
||||||
|
"map_showGuessedLocations": "Zobraziť umiestnenia odhadnutých uzlov",
|
||||||
|
"map_guessedLocation": "Odhadnutá lokalita",
|
||||||
|
"usbScreenTitle": "Pripojte cez USB",
|
||||||
|
"usbScreenSubtitle": "Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vašej MeshCore uzlu.",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenStatus": "Vyberte USB zariadenie",
|
||||||
|
"usbScreenNote": "USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.",
|
||||||
|
"usbScreenEmptyState": "Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte.",
|
||||||
|
"usbErrorPermissionDenied": "Žiadosť o prístup cez USB bola zamietnutá.",
|
||||||
|
"usbErrorDeviceMissing": "Vybrané USB zariadenie už nie je dostupné.",
|
||||||
|
"usbErrorInvalidPort": "Vyberte platné USB zariadenie.",
|
||||||
|
"usbErrorBusy": "Ďalšia požiadavka na pripojenie cez USB je aktuálne v procese.",
|
||||||
|
"usbErrorNotConnected": "Nie je pripojené žiadne USB zariadenie.",
|
||||||
|
"usbErrorOpenFailed": "Nepodarilo sa otvoriť vybrané USB zariadenie.",
|
||||||
|
"usbErrorConnectFailed": "Nepodarilo sa sa sa pripojiť k vybranému USB zariadeniu.",
|
||||||
|
"usbErrorUnsupported": "Podpora USB sériového rozhrania nie je na tejto platforme dostupná.",
|
||||||
|
"usbErrorAlreadyActive": "Pripojenie cez USB je už aktivované.",
|
||||||
|
"usbErrorNoDeviceSelected": "Nebolo vybrané žiadne USB zariadenie.",
|
||||||
|
"usbErrorPortClosed": "Pripojenie cez USB nie je aktivované.",
|
||||||
|
"usbFallbackDeviceName": "Webový sériový zariadenie",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_searching": "Hľadanie USB zariadení...",
|
||||||
|
"usbConnectionFailed": "Neúspešné pripojenie cez USB: {error}",
|
||||||
|
"usbStatus_notConnected": "Vyberte USB zariadenie",
|
||||||
|
"usbStatus_connecting": "Pripojenie k USB zariadeniu...",
|
||||||
|
"usbErrorConnectTimedOut": "Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion.",
|
||||||
|
"@tcpStatus_connectingTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"endpoint": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@tcpConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tcpHostHint": "192.168.40.10",
|
||||||
|
"tcpHostLabel": "IP adresa",
|
||||||
|
"tcpScreenTitle": "Spojte sa pomocou protokolu TCP",
|
||||||
|
"connectionChoiceTcpLabel": "TCP",
|
||||||
|
"tcpPortLabel": "Port",
|
||||||
|
"tcpPortHint": "5000",
|
||||||
|
"tcpStatus_notConnected": "Zadajte cieľovú adresu a pripojte sa.",
|
||||||
|
"tcpStatus_connectingTo": "Pripojenie k {endpoint}...",
|
||||||
|
"tcpErrorHostRequired": "Je potrebné zadať IP adresu.",
|
||||||
|
"tcpErrorPortInvalid": "Číslo portu musí byť medzi 1 a 65535.",
|
||||||
|
"tcpErrorUnsupported": "Prevoz prostredníctvom protokolu TCP nie je na tejto platforme podporovaný.",
|
||||||
|
"tcpErrorTimedOut": "Pripojenie TCP vypršalo.",
|
||||||
|
"tcpConnectionFailed": "Neúspešné vytvorenie TCP spojenia: {error}",
|
||||||
|
"map_showDiscoveryContacts": "Zobraziť kontakty objavov",
|
||||||
|
"map_setAsMyLocation": "Nastavte ako moju polohu",
|
||||||
|
"@path_routeWeight": {
|
||||||
|
"placeholders": {
|
||||||
|
"weight": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"max": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings_privacy": "Nastavenia súkromia",
|
||||||
|
"settings_privacySubtitle": "Ovládni, aké informácie sa zdieľajú.",
|
||||||
|
"settings_telemetryLocationMode": "Režim umiestnenia telemetrie",
|
||||||
|
"settings_telemetryBaseMode": "Základný režim telemetrie",
|
||||||
|
"settings_advertLocation": "Umiestnenie inzerátu",
|
||||||
|
"settings_telemetryEnvironmentMode": "Režim prostredia telemetrie",
|
||||||
|
"settings_advertLocationSubtitle": "Zahrnúť polohu do inzerátu",
|
||||||
|
"settings_allowAll": "Povoliť všetko",
|
||||||
|
"settings_privacySettingsDescription": "Vyberte, ktoré informácie váš zariadenie zdieľa s ostatnými.",
|
||||||
|
"settings_denyAll": "Zamietnuť všetko",
|
||||||
|
"settings_allowByContact": "Povoliť podľa kontaktových vlajok",
|
||||||
|
"contact_info": "Kontaktné informácie",
|
||||||
|
"contact_settings": "Nastavenia kontaktov",
|
||||||
|
"contact_teleBaseSubtitle": "Povoliť zdieľanie úrovne batérie a základnej telemetrie",
|
||||||
|
"contact_teleLoc": "Lokácia telemetrie",
|
||||||
|
"contact_teleLocSubtitle": "Povoliť zdieľanie údajov o lokalite",
|
||||||
|
"contact_teleEnv": "Prostredie telemetrie",
|
||||||
|
"contact_telemetry": "Telemetria",
|
||||||
|
"contact_clearChat": "Vymazať chat",
|
||||||
|
"contact_lastSeen": "Naposledy videný",
|
||||||
|
"contact_teleBase": "Báza telemetrie",
|
||||||
|
"contact_teleEnvSubtitle": "Povoliť zdieľanie údajov senzorov prostredia",
|
||||||
|
"@settings_multiAck": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"appSettings_maxRouteWeightSubtitle": "Maximálna hmotnosť, ktorú môže trás prenášať vďaka úspešným zásielkam.",
|
||||||
|
"appSettings_initialRouteWeightSubtitle": "Počiatočná váha pre nové, objavené cesty",
|
||||||
|
"appSettings_initialRouteWeight": "Počiatočná váha trasy",
|
||||||
|
"appSettings_maxRouteWeight": "Maximálna hmotnosť trasy",
|
||||||
|
"appSettings_routeWeightSuccessIncrement": "Zvyšenie váhy úspechu",
|
||||||
|
"appSettings_routeWeightSuccessIncrementSubtitle": "Hmotnosť pridaná k trase po úspešnej doručení",
|
||||||
|
"appSettings_routeWeightFailureDecrement": "Sníženie váhy, ktorá sa používa na odhad rizika.",
|
||||||
|
"appSettings_routeWeightFailureDecrementSubtitle": "Hmotnosť odstránená z cesty po neúspešnej doručenie",
|
||||||
|
"appSettings_maxMessageRetries": "Maximalný počet pokusov o doručenie správ",
|
||||||
|
"appSettings_maxMessageRetriesSubtitle": "Počet pokusov o odošleť pred označením správy ako neúspešnej",
|
||||||
|
"path_routeWeight": "{weight}/{max}",
|
||||||
|
"settings_telemetryModeUpdated": "Režim telemetrie bol aktualizovaný",
|
||||||
|
"settings_multiAck": "Viaceré ACK: {value}",
|
||||||
|
"map_showOverlaps": "Prekrývanie opakovača kľúča",
|
||||||
|
"map_runTraceWithReturnPath": "Vráťte sa späť po tej istej ceste."
|
||||||
}
|
}
|
||||||
+145
-1
@@ -285,6 +285,7 @@
|
|||||||
"contacts_newGroup": "Nova skupina",
|
"contacts_newGroup": "Nova skupina",
|
||||||
"contacts_groupName": "Ime skupine",
|
"contacts_groupName": "Ime skupine",
|
||||||
"contacts_groupNameRequired": "Ime skupine je obvezno.",
|
"contacts_groupNameRequired": "Ime skupine je obvezno.",
|
||||||
|
"contacts_groupNameReserved": "To ime skupine je rezervirano",
|
||||||
"contacts_groupAlreadyExists": "Skupina \"{name}\" že obstaja",
|
"contacts_groupAlreadyExists": "Skupina \"{name}\" že obstaja",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1605,6 +1606,8 @@
|
|||||||
"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_lastSeen": "Zadnjič videno",
|
||||||
"snrIndicator_nearByRepeaters": "Bližnji ponovitelji",
|
"snrIndicator_nearByRepeaters": "Bližnji ponovitelji",
|
||||||
@@ -1799,5 +1802,146 @@
|
|||||||
"contacts_searchRoomServers": "Išči {number}{str} strežnikov sob...",
|
"contacts_searchRoomServers": "Išči {number}{str} strežnikov sob...",
|
||||||
"contacts_searchContactsNoNumber": "Iskanje stikov...",
|
"contacts_searchContactsNoNumber": "Iskanje stikov...",
|
||||||
"contacts_searchRepeaters": "Išči {number}{str} ponavljalnike...",
|
"contacts_searchRepeaters": "Išči {number}{str} ponavljalnike...",
|
||||||
"contacts_searchUsers": "Išči {number}{str} uporabnikov..."
|
"contacts_searchUsers": "Išči {number}{str} uporabnikov...",
|
||||||
|
"settings_contactSettings": "Nastavitve stika",
|
||||||
|
"contactsSettings_autoAddTitle": "Avtomatsko odkrivanje",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Avtomatsko dodaj uporabnike",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Avtomatsko dodaj ponovitelje",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite ponovitelje.",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Avtomatsko dodaj strežnike sob",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite strežnike sob.",
|
||||||
|
"contactsSettings_otherTitle": "Druge nastavitve v zvezi s stiki",
|
||||||
|
"settings_contactSettingsSubtitle": "Nastavitve za dodajanje stikov.",
|
||||||
|
"contactsSettings_title": "Nastavitve stikov",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Avtomatsko dodaj senzorje",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite uporabnike.",
|
||||||
|
"discoveredContacts_noMatching": "Ni ujemajočih stikov",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite senzorje.",
|
||||||
|
"discoveredContacts_addContact": "Dodaj stik",
|
||||||
|
"discoveredContacts_contactAdded": "Kontakt dodan",
|
||||||
|
"discoveredContacts_copyContact": "Kopiraj stik v odložišče",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Prepiši najstarejše",
|
||||||
|
"discoveredContacts_Title": "Odkriti stiki",
|
||||||
|
"discoveredContacts_searchHint": "Najdeni stiki po iskanju",
|
||||||
|
"discoveredContacts_deleteContact": "Izbriši stik",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Ko je seznam stikov poln, bo najstarejši nestarševski stik zamenjan.",
|
||||||
|
"common_deleteAll": "Izbriši vse",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Ste prepričani, da želite izbrisati vse odkrite kontakte?",
|
||||||
|
"discoveredContacts_deleteContactAll": "Izbriši vse odkrite kontakte",
|
||||||
|
"map_guessedLocation": "Predpostavljena lokacija",
|
||||||
|
"map_showGuessedLocations": "Pokaži lokacije domnevnih not.",
|
||||||
|
"usbScreenTitle": "Povežite preko USB",
|
||||||
|
"usbScreenSubtitle": "Izberite zaznano serijsko napravo in se neposredno povežite z vašo MeshCore napravo.",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenStatus": "Izberite USB naprave",
|
||||||
|
"usbScreenNote": "USB serijska povezava je aktivna na podprtih napravah Android in na desktop platformah.",
|
||||||
|
"usbScreenEmptyState": "Niti en USB naprave niso najdeni. Povežite eno in posodobite.",
|
||||||
|
"usbErrorPermissionDenied": "Dovoljenje za dostop preko USB-ja je bilo zavrnjeno.",
|
||||||
|
"usbErrorDeviceMissing": "Izbrani USB napravej je več ne.",
|
||||||
|
"usbErrorInvalidPort": "Izberite veljavno USB naprave.",
|
||||||
|
"usbErrorBusy": "Že je v teku zahteva za povezavo preko USB.",
|
||||||
|
"usbErrorNotConnected": "Ni priklopljenih USB naprave.",
|
||||||
|
"usbErrorOpenFailed": "Uspešno ni bilo mogo, da se odpre izbran naprave USB.",
|
||||||
|
"usbErrorConnectFailed": "Niso bilo mogoče uskladiti povezave z izbranim USB napom.",
|
||||||
|
"usbErrorUnsupported": "USB serijska komunikacija ni podprta na tej platformi.",
|
||||||
|
"usbErrorAlreadyActive": "USB povezava je že aktivirana.",
|
||||||
|
"usbErrorNoDeviceSelected": "Ni bilo izbranega USB naprave.",
|
||||||
|
"usbErrorPortClosed": "USB povezava ni aktivirana.",
|
||||||
|
"usbFallbackDeviceName": "Naprave za serijsko komunikacijo preko spleta",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_notConnected": "Izberite USB naprave.",
|
||||||
|
"usbStatus_connecting": "Povezava z USB napravo...",
|
||||||
|
"usbStatus_searching": "Iskanje USB naprav...",
|
||||||
|
"usbConnectionFailed": "Napaka pri povezavi preko USB: {error}",
|
||||||
|
"usbErrorConnectTimedOut": "Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion.",
|
||||||
|
"@tcpStatus_connectingTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"endpoint": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@tcpConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"connectionChoiceTcpLabel": "TCP",
|
||||||
|
"tcpHostLabel": "IP naslov",
|
||||||
|
"tcpHostHint": "192.168.40.10",
|
||||||
|
"tcpScreenTitle": "Komunicirajte preko protokola TCP",
|
||||||
|
"tcpPortLabel": "Vrata",
|
||||||
|
"tcpPortHint": "5000",
|
||||||
|
"tcpStatus_notConnected": "Vnesite končni naslov in se povežite",
|
||||||
|
"tcpStatus_connectingTo": "Povezava z {endpoint}...",
|
||||||
|
"tcpErrorHostRequired": "Potrebna je IP-naslov.",
|
||||||
|
"tcpErrorPortInvalid": "Port mora biti med 1 in 65535.",
|
||||||
|
"tcpErrorUnsupported": "Transport preko protokola TCP ni podprt na tej platformi.",
|
||||||
|
"tcpErrorTimedOut": "Povezava TCP je presegla časovno obdobje.",
|
||||||
|
"tcpConnectionFailed": "Napaka pri povezavi TCP: {error}",
|
||||||
|
"map_showDiscoveryContacts": "Prikaži odkritja kontaktov",
|
||||||
|
"map_setAsMyLocation": "Nastavite to kot mojo lokacijo",
|
||||||
|
"@path_routeWeight": {
|
||||||
|
"placeholders": {
|
||||||
|
"weight": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"max": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings_privacy": "Nastavitve zasebnosti",
|
||||||
|
"settings_privacySettingsDescription": "Izberite, katere informacije vaš naprava deli z drugimi.",
|
||||||
|
"settings_telemetryBaseMode": "Osnovni način telemetrije",
|
||||||
|
"settings_telemetryLocationMode": "Način delovanja telemetrije",
|
||||||
|
"settings_telemetryEnvironmentMode": "Način delovanja okolja telemetrije",
|
||||||
|
"settings_advertLocation": "Lokacija oglasa",
|
||||||
|
"settings_allowByContact": "Dovoli po kontaktnih zastavah",
|
||||||
|
"settings_denyAll": "Zavrniti vse",
|
||||||
|
"settings_allowAll": "Dovoli vse",
|
||||||
|
"settings_privacySubtitle": "Kontrolirajte, katere informacije so deljene.",
|
||||||
|
"contact_info": "Kontaktni podatki",
|
||||||
|
"contact_teleBase": "Baza telemetrije",
|
||||||
|
"contact_teleBaseSubtitle": "Dovoli deljenje stanja baterije in osnovne telemetrije",
|
||||||
|
"contact_teleLoc": "Lokacija telemetrije",
|
||||||
|
"contact_lastSeen": "Zadnjič videno",
|
||||||
|
"contact_settings": "Nastavitve stika",
|
||||||
|
"settings_advertLocationSubtitle": "Vključi lokacijo v oglas.",
|
||||||
|
"contact_telemetry": "Telemetrija",
|
||||||
|
"contact_clearChat": "Počisti klepet",
|
||||||
|
"contact_teleEnv": "Okolje telemetrije",
|
||||||
|
"contact_teleEnvSubtitle": "Dovoli deljenje podatkov okoljskih senzorjev",
|
||||||
|
"contact_teleLocSubtitle": "Dovoli deljenje podatkov o lokaciji",
|
||||||
|
"@settings_multiAck": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"appSettings_maxRouteWeightSubtitle": "Največja teža, ki jo lahko pot doseže s uspešnimi dostavnami.",
|
||||||
|
"appSettings_initialRouteWeight": "Izvirna teža poti",
|
||||||
|
"appSettings_initialRouteWeightSubtitle": "Izguba teže za nove, odkriti poti",
|
||||||
|
"appSettings_maxRouteWeight": "Največja dovoljena teža poti",
|
||||||
|
"appSettings_routeWeightSuccessIncrement": "Učinkovitost: povečanje",
|
||||||
|
"appSettings_routeWeightSuccessIncrementSubtitle": "Težava, dodana poti po uspešni dostavi",
|
||||||
|
"appSettings_routeWeightFailureDecrement": "Zmanjšanje teže, ki je povezana s pomanjkanjem",
|
||||||
|
"appSettings_routeWeightFailureDecrementSubtitle": "Težo, ki ni bila uspešno dostavljena, odstranili s poti.",
|
||||||
|
"appSettings_maxMessageRetries": "Najve število poskusov pošiljanja sporočil",
|
||||||
|
"appSettings_maxMessageRetriesSubtitle": "Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno",
|
||||||
|
"path_routeWeight": "{weight}/{max}",
|
||||||
|
"settings_multiAck": "Večkratni potrditvi: {value}",
|
||||||
|
"settings_telemetryModeUpdated": "Način telemetrije posodobljen",
|
||||||
|
"map_showOverlaps": "Prekrivanje ključa ponovnega predvajanja",
|
||||||
|
"map_runTraceWithReturnPath": "Vrni se nazaj po isti poti."
|
||||||
}
|
}
|
||||||
+145
-1
@@ -285,6 +285,7 @@
|
|||||||
"contacts_newGroup": "Ny grupp",
|
"contacts_newGroup": "Ny grupp",
|
||||||
"contacts_groupName": "Gruppnamn",
|
"contacts_groupName": "Gruppnamn",
|
||||||
"contacts_groupNameRequired": "Gruppnamnet är obligatoriskt",
|
"contacts_groupNameRequired": "Gruppnamnet är obligatoriskt",
|
||||||
|
"contacts_groupNameReserved": "Detta gruppnamn är reserverat",
|
||||||
"contacts_groupAlreadyExists": "Gruppen \"{name}\" finns redan.",
|
"contacts_groupAlreadyExists": "Gruppen \"{name}\" finns redan.",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1605,6 +1606,8 @@
|
|||||||
"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_lastSeen": "Senast sedd",
|
||||||
"snrIndicator_nearByRepeaters": "Närliggande uppreparstationer",
|
"snrIndicator_nearByRepeaters": "Närliggande uppreparstationer",
|
||||||
@@ -1799,5 +1802,146 @@
|
|||||||
"contacts_searchRepeaters": "Sök {number}{str} upprepningsenheter...",
|
"contacts_searchRepeaters": "Sök {number}{str} upprepningsenheter...",
|
||||||
"contacts_searchFavorites": "Sök {number}{str} Favoriter...",
|
"contacts_searchFavorites": "Sök {number}{str} Favoriter...",
|
||||||
"contacts_searchUsers": "Sök {number}{str} användare...",
|
"contacts_searchUsers": "Sök {number}{str} användare...",
|
||||||
"contacts_searchRoomServers": "Sök {number}{str} Room-servrar..."
|
"contacts_searchRoomServers": "Sök {number}{str} Room-servrar...",
|
||||||
|
"settings_contactSettingsSubtitle": "Inställningar för hur kontakter läggs till.",
|
||||||
|
"settings_contactSettings": "Kontaktinställningar",
|
||||||
|
"contactsSettings_autoAddTitle": "Automatisk upptäckt",
|
||||||
|
"contactsSettings_otherTitle": "Andra inställningar relaterade till kontakt",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta användare",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Lägg till upprepande enheter automatiskt",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta rumsservrar.",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Lägg till sensorer automatiskt",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Lägg till användare automatiskt",
|
||||||
|
"contactsSettings_title": "Kontaktinställningar",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta sensorer.",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Skriv över äldst",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta repeater.",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Lägg automatiskt till rumsservrar",
|
||||||
|
"discoveredContacts_noMatching": "Inga matchande kontakter",
|
||||||
|
"discoveredContacts_searchHint": "Sök uppfunna kontakter",
|
||||||
|
"discoveredContacts_deleteContact": "Ta bort kontakt",
|
||||||
|
"discoveredContacts_Title": "Upptäckta kontakter",
|
||||||
|
"discoveredContacts_contactAdded": "Kontakt tillagd",
|
||||||
|
"discoveredContacts_addContact": "Lägg till kontakt",
|
||||||
|
"discoveredContacts_copyContact": "Kopiera kontakt till urklipp",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "När kontaktlistan är full ersätts den äldsta icke-favoriterade kontakten.",
|
||||||
|
"common_deleteAll": "Ta bort alla",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Är du säker på att du vill ta bort alla upptäckta kontakter?",
|
||||||
|
"discoveredContacts_deleteContactAll": "Ta bort alla upptäckta kontakter",
|
||||||
|
"map_guessedLocation": "Gissad plats",
|
||||||
|
"map_showGuessedLocations": "Visa upp de antagna nodernas placeringar",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenSubtitle": "Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.",
|
||||||
|
"usbScreenTitle": "Anslut via USB",
|
||||||
|
"usbScreenStatus": "Välj en USB-enhet",
|
||||||
|
"usbScreenNote": "USB-seriell kommunikation är aktiv på stödda Android-enheter och på skrivbordsplattformar.",
|
||||||
|
"usbScreenEmptyState": "Inga USB-enheter hittades. Anslut en och uppdatera.",
|
||||||
|
"usbErrorPermissionDenied": "Tillgången via USB nekas.",
|
||||||
|
"usbErrorDeviceMissing": "Den valda USB-enheten är inte längre tillgänglig.",
|
||||||
|
"usbErrorInvalidPort": "Välj en giltig USB-enhet.",
|
||||||
|
"usbErrorBusy": "En annan förfrågan om USB-anslutning är redan pågående.",
|
||||||
|
"usbErrorNotConnected": "Ingen USB-enhet är ansluten.",
|
||||||
|
"usbErrorOpenFailed": "Misslyckades med att öppna det valda USB-enheten.",
|
||||||
|
"usbErrorConnectFailed": "Kunde inte ansluta till det valda USB-enheten.",
|
||||||
|
"usbErrorUnsupported": "USB-seriell kommunikation stöds inte på denna plattform.",
|
||||||
|
"usbErrorAlreadyActive": "En USB-anslutning är redan aktiv.",
|
||||||
|
"usbErrorNoDeviceSelected": "Ingen USB-enhet valdes.",
|
||||||
|
"usbErrorPortClosed": "USB-anslutningen är inte aktiv.",
|
||||||
|
"usbFallbackDeviceName": "Web-serieenhet",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_connecting": "Anslutning till USB-enhet...",
|
||||||
|
"usbStatus_notConnected": "Välj en USB-enhet",
|
||||||
|
"usbConnectionFailed": "Fel vid USB-anslutning: {error}",
|
||||||
|
"usbStatus_searching": "Söker efter USB-enheter...",
|
||||||
|
"usbErrorConnectTimedOut": "Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware.",
|
||||||
|
"@tcpStatus_connectingTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"endpoint": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@tcpConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tcpHostHint": "192.168.40.10",
|
||||||
|
"tcpHostLabel": "IP-adress",
|
||||||
|
"tcpScreenTitle": "Anslut via TCP",
|
||||||
|
"connectionChoiceTcpLabel": "TCP",
|
||||||
|
"tcpPortLabel": "Port",
|
||||||
|
"tcpPortHint": "5000",
|
||||||
|
"tcpStatus_notConnected": "Ange slutpunkt och anslut",
|
||||||
|
"tcpStatus_connectingTo": "Anslutning till {endpoint}...",
|
||||||
|
"tcpErrorHostRequired": "IP-adress krävs.",
|
||||||
|
"tcpErrorPortInvalid": "Porten måste vara mellan 1 och 65535.",
|
||||||
|
"tcpErrorUnsupported": "TCP-transport fungerar inte på denna plattform.",
|
||||||
|
"tcpErrorTimedOut": "TCP-anslutningen har tidsut gått.",
|
||||||
|
"tcpConnectionFailed": "Fel vid TCP-anslutning: {error}",
|
||||||
|
"map_showDiscoveryContacts": "Visa Discovery-kontakter",
|
||||||
|
"map_setAsMyLocation": "Ange som min plats",
|
||||||
|
"@path_routeWeight": {
|
||||||
|
"placeholders": {
|
||||||
|
"weight": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"max": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings_privacy": "Inställningar för sekretess",
|
||||||
|
"settings_allowAll": "Tillåt alla",
|
||||||
|
"settings_privacySubtitle": "Kontrollera vilken information som delas.",
|
||||||
|
"settings_telemetryEnvironmentMode": "Telemetri miljöläge",
|
||||||
|
"settings_telemetryBaseMode": "Telemetribasläge",
|
||||||
|
"settings_telemetryLocationMode": "Telemetritillstånd för plats",
|
||||||
|
"settings_advertLocation": "Annonsplacering",
|
||||||
|
"contact_info": "Kontaktinformation",
|
||||||
|
"contact_settings": "Kontaktinställningar",
|
||||||
|
"contact_telemetry": "Telemetri",
|
||||||
|
"settings_denyAll": "Neka alla",
|
||||||
|
"settings_allowByContact": "Tillåt via kontaktflaggor",
|
||||||
|
"settings_privacySettingsDescription": "Välj vilken information din enhet delar med andra.",
|
||||||
|
"contact_lastSeen": "Senast sedd",
|
||||||
|
"contact_clearChat": "Rensa Chatt",
|
||||||
|
"contact_teleEnv": "Telemetri Miljö",
|
||||||
|
"settings_advertLocationSubtitle": "Inkludera plats i annonsen",
|
||||||
|
"contact_teleEnvSubtitle": "Tillåt delning av miljösensordata",
|
||||||
|
"contact_teleBase": "Telemetribas",
|
||||||
|
"contact_teleBaseSubtitle": "Tillåt delning av batterinivå och grundläggande telemetri",
|
||||||
|
"contact_teleLoc": "Telemetridata plats",
|
||||||
|
"contact_teleLocSubtitle": "Tillåt delning av platsdata",
|
||||||
|
"@settings_multiAck": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"appSettings_initialRouteWeightSubtitle": "Initial vikt för nyligen upptäckta vägar",
|
||||||
|
"appSettings_maxRouteWeight": "Maximalt tillåtet vikt för rutten",
|
||||||
|
"appSettings_maxRouteWeightSubtitle": "Maximal vikt som en leveransväg kan ackumulera från framgångsrika leveranser.",
|
||||||
|
"appSettings_initialRouteWeight": "Initial vikt för rutt",
|
||||||
|
"appSettings_routeWeightSuccessIncrement": "Ökning av vikt för framgång",
|
||||||
|
"appSettings_routeWeightSuccessIncrementSubtitle": "Vikt läggs till en väg efter en lyckad leverans.",
|
||||||
|
"appSettings_routeWeightFailureDecrement": "Minskning av vikten för misslyckande",
|
||||||
|
"appSettings_routeWeightFailureDecrementSubtitle": "Vikt som tagits bort från en väg efter ett misslyckat leveransförsök",
|
||||||
|
"appSettings_maxMessageRetries": "Maximalt antal försök",
|
||||||
|
"appSettings_maxMessageRetriesSubtitle": "Antal försök att skicka om ett meddelande innan det markeras som misslyckat.",
|
||||||
|
"path_routeWeight": "{weight}/{max}",
|
||||||
|
"settings_telemetryModeUpdated": "Telemetri-läge uppdaterat",
|
||||||
|
"settings_multiAck": "Multi-ACKs: {value}",
|
||||||
|
"map_showOverlaps": "Repeater-nyckelöverlappningar",
|
||||||
|
"map_runTraceWithReturnPath": "Gå tillbaka på samma väg"
|
||||||
}
|
}
|
||||||
+145
-1
@@ -286,6 +286,7 @@
|
|||||||
"contacts_newGroup": "Нова група",
|
"contacts_newGroup": "Нова група",
|
||||||
"contacts_groupName": "Назва групи",
|
"contacts_groupName": "Назва групи",
|
||||||
"contacts_groupNameRequired": "Назва групи обов'язкова.",
|
"contacts_groupNameRequired": "Назва групи обов'язкова.",
|
||||||
|
"contacts_groupNameReserved": "Ця назва групи зарезервована",
|
||||||
"contacts_groupAlreadyExists": "Група «{name}» вже існує.",
|
"contacts_groupAlreadyExists": "Група «{name}» вже існує.",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1605,6 +1606,8 @@
|
|||||||
"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_lastSeen": "Останній раз бачили",
|
||||||
"snrIndicator_nearByRepeaters": "Ближні ретранслятори",
|
"snrIndicator_nearByRepeaters": "Ближні ретранслятори",
|
||||||
@@ -1799,5 +1802,146 @@
|
|||||||
"contacts_searchFavorites": "Пошук {number}{str} улюблених...",
|
"contacts_searchFavorites": "Пошук {number}{str} улюблених...",
|
||||||
"contacts_searchContactsNoNumber": "Пошук контактів...",
|
"contacts_searchContactsNoNumber": "Пошук контактів...",
|
||||||
"contacts_searchRepeaters": "Пошук {number}{str} ретрансляторів...",
|
"contacts_searchRepeaters": "Пошук {number}{str} ретрансляторів...",
|
||||||
"contacts_unread": "Непрочитане"
|
"contacts_unread": "Непрочитане",
|
||||||
|
"settings_contactSettingsSubtitle": "Налаштування для додавання контактів",
|
||||||
|
"settings_contactSettings": "Налаштування контактів",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Дозволити супутникові автоматично додавати виявлених користувачів",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Автоматично додавати повторювачі",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Дозволити супутнику автоматично додавати виявлені ретранслятори",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Автоматично додавати сервери кімнат",
|
||||||
|
"contactsSettings_otherTitle": "Інші налаштування, пов'язані з контактами",
|
||||||
|
"contactsSettings_autoAddTitle": "Автоматичне виявлення",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Автоматично додавати користувачів",
|
||||||
|
"contactsSettings_title": "Налаштування контактів",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Дозволити супровіднику автоматично додавати виявлені сервери кімнат.",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Автоматично додавати датчики",
|
||||||
|
"discoveredContacts_searchHint": "Знайти виявлені контакти",
|
||||||
|
"discoveredContacts_contactAdded": "Контакт додано",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Дозволити супровіднику автоматично додавати виявлені сенсори",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Перезаписати найстаріше",
|
||||||
|
"discoveredContacts_Title": "Виявлені контакти",
|
||||||
|
"discoveredContacts_noMatching": "Відповідних контактів не знайдено",
|
||||||
|
"discoveredContacts_deleteContact": "Видалити контакт",
|
||||||
|
"discoveredContacts_copyContact": "Копіювати контакт у буфер обміну",
|
||||||
|
"discoveredContacts_addContact": "Додати контакт",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Коли список контактів заповнений, найстарший контакт без позначки улюбленого буде замінений.",
|
||||||
|
"common_deleteAll": "Видалити все",
|
||||||
|
"discoveredContacts_deleteContactAll": "Видалити всі виявлені контакти",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Ви впевнені, що хочете видалити всі виявлені контакти?",
|
||||||
|
"map_showGuessedLocations": "Показати місцезнаходження передбачених вузлів",
|
||||||
|
"map_guessedLocation": "Визначено місцезнаходження",
|
||||||
|
"usbScreenSubtitle": "Виберіть виявлене серійне пристрій і підключіть його безпосередньо до вашого вузла MeshCore.",
|
||||||
|
"usbScreenTitle": "Підключити через USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenStatus": "Виберіть пристрій USB",
|
||||||
|
"usbScreenNote": "USB-серіальний інтерфейс активний на підтримуваних пристроях на базі Android та на десктопних платформах.",
|
||||||
|
"usbScreenEmptyState": "Не знайдено жодних пристроїв USB. Підключіть один і перезавантажте.",
|
||||||
|
"usbErrorPermissionDenied": "Було відмовлено у наданні дозволу на використання USB.",
|
||||||
|
"usbErrorDeviceMissing": "Вибране USB-пристрій більше недоступне.",
|
||||||
|
"usbErrorInvalidPort": "Виберіть дійсний USB-пристрій.",
|
||||||
|
"usbErrorBusy": "Ще один запит на підключення через USB вже обробляється.",
|
||||||
|
"usbErrorNotConnected": "Немає підключених пристроїв USB.",
|
||||||
|
"usbErrorOpenFailed": "Не вдалося відкрити вибране USB-пристрій.",
|
||||||
|
"usbErrorConnectFailed": "Не вдалося підключитися до вибраного USB-пристрою.",
|
||||||
|
"usbErrorUnsupported": "Підтримка USB-серіального інтерфейсу не реалізована на цій платформі.",
|
||||||
|
"usbErrorAlreadyActive": "USB-з'єднання вже встановлено.",
|
||||||
|
"usbErrorNoDeviceSelected": "Не було вибрано жодного пристрою USB.",
|
||||||
|
"usbErrorPortClosed": "З'єднання USB не встановлено.",
|
||||||
|
"usbFallbackDeviceName": "Пристрій для передачі даних по веб-серіалах",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_searching": "Пошук пристроїв USB...",
|
||||||
|
"usbStatus_notConnected": "Виберіть пристрій USB",
|
||||||
|
"usbConnectionFailed": "Не вдалося встановити з'єднання через USB: {error}",
|
||||||
|
"usbStatus_connecting": "Підключення до USB-пристрою...",
|
||||||
|
"usbErrorConnectTimedOut": "З'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion.",
|
||||||
|
"@tcpStatus_connectingTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"endpoint": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@tcpConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"connectionChoiceTcpLabel": "TCP",
|
||||||
|
"tcpHostHint": "192.168.40.10",
|
||||||
|
"tcpHostLabel": "IP-адреса",
|
||||||
|
"tcpScreenTitle": "З'єднатися через протокол TCP",
|
||||||
|
"tcpPortLabel": "Порт",
|
||||||
|
"tcpPortHint": "5000",
|
||||||
|
"tcpStatus_notConnected": "Введіть кінцеву точку та підключіться",
|
||||||
|
"tcpStatus_connectingTo": "Підключення до {endpoint}...",
|
||||||
|
"tcpErrorHostRequired": "Необхідно вказати IP-адресу.",
|
||||||
|
"tcpErrorPortInvalid": "Порт повинен бути в межах від 1 до 65535.",
|
||||||
|
"tcpErrorUnsupported": "Транспорт TCP не підтримується на цій платформі.",
|
||||||
|
"tcpErrorTimedOut": "З'єднання TCP завершилося через закінчення часу очікування.",
|
||||||
|
"tcpConnectionFailed": "Не вдалося встановити з'єднання TCP: {error}",
|
||||||
|
"map_showDiscoveryContacts": "Показати контакти Відкриття",
|
||||||
|
"map_setAsMyLocation": "Встановити моє місцезнаходження",
|
||||||
|
"@path_routeWeight": {
|
||||||
|
"placeholders": {
|
||||||
|
"weight": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"max": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings_privacySubtitle": "Керуйте інформацією, яку буде спільно використовуватися",
|
||||||
|
"settings_privacy": "Налаштування приватності",
|
||||||
|
"settings_telemetryBaseMode": "Режим базової телеметрії",
|
||||||
|
"settings_telemetryLocationMode": "Режим місця телеметрії",
|
||||||
|
"settings_advertLocation": "Розміщення реклами",
|
||||||
|
"settings_advertLocationSubtitle": "Включити місце розташування в оголошення",
|
||||||
|
"settings_privacySettingsDescription": "Виберіть, яку інформацію ваш пристрій буде передавати іншим.",
|
||||||
|
"settings_allowAll": "Дозволити все",
|
||||||
|
"settings_denyAll": "Відхилити все",
|
||||||
|
"settings_allowByContact": "Дозволити за контактними прапорцями",
|
||||||
|
"settings_telemetryEnvironmentMode": "Режим середовища телеметрії",
|
||||||
|
"contact_info": "Контактна інформація",
|
||||||
|
"contact_teleBaseSubtitle": "Дозволити спільний доступ до рівня заряду батареї та базової телеметрії",
|
||||||
|
"contact_teleLoc": "Розташування телеметрії",
|
||||||
|
"contact_teleBase": "Базовий телебачення",
|
||||||
|
"contact_teleLocSubtitle": "Дозволити спільне використання даних про місцеположення",
|
||||||
|
"contact_settings": "Налаштування контактів",
|
||||||
|
"contact_telemetry": "Телеметрія",
|
||||||
|
"contact_clearChat": "Очистити чат",
|
||||||
|
"contact_lastSeen": "Останній раз бачили",
|
||||||
|
"contact_teleEnv": "Середовище телеметрії",
|
||||||
|
"contact_teleEnvSubtitle": "Дозволити спільний доступ до даних датчиків середовища",
|
||||||
|
"@settings_multiAck": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"appSettings_initialRouteWeight": "Початкова вартість маршруту",
|
||||||
|
"appSettings_initialRouteWeightSubtitle": "Початкова вага для нових відкритих шляхів",
|
||||||
|
"appSettings_maxRouteWeight": "Максимальна вага маршруту",
|
||||||
|
"appSettings_maxRouteWeightSubtitle": "Максимальна вага, яку може накопичити маршрут завдяки успішним доставкам.",
|
||||||
|
"appSettings_routeWeightSuccessIncrement": "Збільшення ваги успіху",
|
||||||
|
"appSettings_routeWeightSuccessIncrementSubtitle": "Вага, додана до маршруту після успішної доставки",
|
||||||
|
"appSettings_routeWeightFailureDecrement": "Зменшення ваги помилки",
|
||||||
|
"appSettings_routeWeightFailureDecrementSubtitle": "Вага, яка була знята з маршруту після невдалої доставки",
|
||||||
|
"appSettings_maxMessageRetries": "Максимальна кількість повторних спроб надсилання повідомлення",
|
||||||
|
"appSettings_maxMessageRetriesSubtitle": "Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале",
|
||||||
|
"path_routeWeight": "{weight}/{max}",
|
||||||
|
"settings_telemetryModeUpdated": "Режим телеметрії оновлено",
|
||||||
|
"settings_multiAck": "Багатократне підтвердження: {value}",
|
||||||
|
"map_showOverlaps": "Перекриття ключа повторювача",
|
||||||
|
"map_runTraceWithReturnPath": "Повернутися назад тим же шляхом"
|
||||||
}
|
}
|
||||||
+145
-1
@@ -300,6 +300,7 @@
|
|||||||
"contacts_newGroup": "新建群聊",
|
"contacts_newGroup": "新建群聊",
|
||||||
"contacts_groupName": "群聊名称",
|
"contacts_groupName": "群聊名称",
|
||||||
"contacts_groupNameRequired": "请输入群聊名称",
|
"contacts_groupNameRequired": "请输入群聊名称",
|
||||||
|
"contacts_groupNameReserved": "该群组名称已被保留",
|
||||||
"contacts_groupAlreadyExists": "名为 \"{name}\" 的群聊已存在",
|
"contacts_groupAlreadyExists": "名为 \"{name}\" 的群聊已存在",
|
||||||
"@contacts_groupAlreadyExists": {
|
"@contacts_groupAlreadyExists": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1609,6 +1610,8 @@
|
|||||||
"map_removeLast": "移除最后一个",
|
"map_removeLast": "移除最后一个",
|
||||||
"map_runTrace": "运行路径追踪",
|
"map_runTrace": "运行路径追踪",
|
||||||
"scanner_bluetoothOffMessage": "请开启蓝牙以搜索设备",
|
"scanner_bluetoothOffMessage": "请开启蓝牙以搜索设备",
|
||||||
|
"scanner_chromeRequired": "需要 Chrome 浏览器",
|
||||||
|
"scanner_chromeRequiredMessage": "此 Web 应用程序需要 Google Chrome 或基于 Chromium 的浏览器以支持蓝牙。",
|
||||||
"scanner_bluetoothOff": "蓝牙已关闭",
|
"scanner_bluetoothOff": "蓝牙已关闭",
|
||||||
"scanner_enableBluetooth": "启用蓝牙",
|
"scanner_enableBluetooth": "启用蓝牙",
|
||||||
"snrIndicator_lastSeen": "最近访问",
|
"snrIndicator_lastSeen": "最近访问",
|
||||||
@@ -1804,5 +1807,146 @@
|
|||||||
"contacts_searchRepeaters": "搜索 {number}{str} 重复器...",
|
"contacts_searchRepeaters": "搜索 {number}{str} 重复器...",
|
||||||
"contacts_searchContactsNoNumber": "搜索联系人...",
|
"contacts_searchContactsNoNumber": "搜索联系人...",
|
||||||
"contacts_searchRoomServers": "搜索 {number}{str} 房间服务器...",
|
"contacts_searchRoomServers": "搜索 {number}{str} 房间服务器...",
|
||||||
"contacts_searchFavorites": "搜索 {number}{str} 收藏..."
|
"contacts_searchFavorites": "搜索 {number}{str} 收藏...",
|
||||||
|
"settings_contactSettings": "联系人设置",
|
||||||
|
"contactsSettings_title": "联系人设置",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "自动添加用户",
|
||||||
|
"contactsSettings_otherTitle": "其他联系人相关设置",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "允许伴侣自动添加发现的用户",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "允许伴侣自动添加发现的重复器",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "自动添加传感器",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "允许伴侣自动添加发现的房间服务器",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "自动添加重复器",
|
||||||
|
"contactsSettings_autoAddTitle": "自动发现",
|
||||||
|
"settings_contactSettingsSubtitle": "添加联系人的设置",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "覆盖最旧的",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "允许伴侣自动添加发现的传感器",
|
||||||
|
"discoveredContacts_searchHint": "搜索已发现的联系人",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "自动添加房间服务器",
|
||||||
|
"discoveredContacts_contactAdded": "联系人已添加",
|
||||||
|
"discoveredContacts_deleteContact": "删除联系人",
|
||||||
|
"discoveredContacts_addContact": "添加联系人",
|
||||||
|
"discoveredContacts_noMatching": "没有匹配的联系人",
|
||||||
|
"discoveredContacts_Title": "已发现的联系人",
|
||||||
|
"discoveredContacts_copyContact": "复制联系人到剪贴板",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "当联系人列表已满时,将替换最老的非收藏联系人。",
|
||||||
|
"common_deleteAll": "删除全部",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "您确定要删除所有发现的联系人吗?",
|
||||||
|
"discoveredContacts_deleteContactAll": "删除所有发现的联系人",
|
||||||
|
"map_showGuessedLocations": "显示猜测的节点位置",
|
||||||
|
"map_guessedLocation": "猜测的位置",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenTitle": "通过USB连接",
|
||||||
|
"usbScreenSubtitle": "选择已检测到的串行设备,并直接连接到您的 MeshCore 节点。",
|
||||||
|
"connectionChoiceBluetoothLabel": "蓝牙",
|
||||||
|
"usbScreenStatus": "选择一个 USB 设备",
|
||||||
|
"usbScreenNote": "USB 串行接口在支持的 Android 设备和桌面平台上处于活动状态。",
|
||||||
|
"usbScreenEmptyState": "未找到任何 USB 设备。请插入一个,然后刷新。",
|
||||||
|
"usbErrorPermissionDenied": "拒绝了USB权限。",
|
||||||
|
"usbErrorDeviceMissing": "所选的USB设备已不再可用。",
|
||||||
|
"usbErrorInvalidPort": "选择一个有效的USB设备。",
|
||||||
|
"usbErrorBusy": "还有一个 USB 连接请求正在进行中。",
|
||||||
|
"usbErrorNotConnected": "没有连接任何USB设备。",
|
||||||
|
"usbErrorOpenFailed": "未能打开所选的USB设备。",
|
||||||
|
"usbErrorConnectFailed": "未能连接到所选的USB设备。",
|
||||||
|
"usbErrorUnsupported": "此平台不支持USB串行通信。",
|
||||||
|
"usbErrorAlreadyActive": "USB 连接已建立。",
|
||||||
|
"usbErrorNoDeviceSelected": "未选择任何 USB 设备。",
|
||||||
|
"usbErrorPortClosed": "USB 连接未建立。",
|
||||||
|
"usbFallbackDeviceName": "Web 串流设备",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_searching": "正在搜索 USB 设备...",
|
||||||
|
"usbStatus_connecting": "连接USB设备...",
|
||||||
|
"usbStatus_notConnected": "选择一个 USB 设备",
|
||||||
|
"usbConnectionFailed": "USB 连接失败:{error}",
|
||||||
|
"usbErrorConnectTimedOut": "连接超时。请确保设备已安装 USB 伴侣固件。",
|
||||||
|
"@tcpStatus_connectingTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"endpoint": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@tcpConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tcpHostLabel": "IP地址",
|
||||||
|
"tcpHostHint": "192.168.40.10",
|
||||||
|
"tcpScreenTitle": "通过 TCP 连接",
|
||||||
|
"connectionChoiceTcpLabel": "TCP",
|
||||||
|
"tcpPortLabel": "端口",
|
||||||
|
"tcpPortHint": "5000",
|
||||||
|
"tcpStatus_notConnected": "输入目标地址,然后连接",
|
||||||
|
"tcpStatus_connectingTo": "连接到 {endpoint}...",
|
||||||
|
"tcpErrorHostRequired": "需要提供IP地址。",
|
||||||
|
"tcpErrorPortInvalid": "端口号必须在 1 到 65535 之间。",
|
||||||
|
"tcpErrorUnsupported": "此平台不支持 TCP 传输。",
|
||||||
|
"tcpErrorTimedOut": "TCP 连接超时。",
|
||||||
|
"tcpConnectionFailed": "TCP 连接失败:{error}",
|
||||||
|
"map_showDiscoveryContacts": "显示发现联系人",
|
||||||
|
"map_setAsMyLocation": "设置为我的位置",
|
||||||
|
"@path_routeWeight": {
|
||||||
|
"placeholders": {
|
||||||
|
"weight": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"max": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings_privacySubtitle": "控制要共享的信息。",
|
||||||
|
"settings_privacySettingsDescription": "选择您的设备与他人共享的信息。",
|
||||||
|
"settings_telemetryBaseMode": "遥测基础模式",
|
||||||
|
"settings_telemetryLocationMode": "遥测位置模式",
|
||||||
|
"settings_advertLocation": "广告位置",
|
||||||
|
"settings_advertLocationSubtitle": "在广告中包含位置",
|
||||||
|
"settings_allowByContact": "按联系人标志允许",
|
||||||
|
"settings_denyAll": "拒绝所有",
|
||||||
|
"settings_privacy": "隐私设置",
|
||||||
|
"settings_allowAll": "允许全部",
|
||||||
|
"contact_info": "联系信息",
|
||||||
|
"contact_teleBase": "遥测基站",
|
||||||
|
"contact_teleBaseSubtitle": "允许共享电池电量和基本遥测数据",
|
||||||
|
"settings_telemetryEnvironmentMode": "遥测环境模式",
|
||||||
|
"contact_teleLoc": "遥测位置",
|
||||||
|
"contact_teleEnv": "遥测环境",
|
||||||
|
"contact_teleEnvSubtitle": "允许共享环境传感器数据",
|
||||||
|
"contact_clearChat": "清除聊天记录",
|
||||||
|
"contact_lastSeen": "最近出现",
|
||||||
|
"contact_settings": "联系人设置",
|
||||||
|
"contact_teleLocSubtitle": "允许共享位置数据",
|
||||||
|
"contact_telemetry": "遥测数据",
|
||||||
|
"@settings_multiAck": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"appSettings_maxRouteWeight": "最大路径重量",
|
||||||
|
"appSettings_initialRouteWeightSubtitle": "新发现路径的初始重量",
|
||||||
|
"appSettings_initialRouteWeight": "初始路线权重",
|
||||||
|
"appSettings_maxRouteWeightSubtitle": "一条路径可以累积的最大重量,取决于成功交付的数量。",
|
||||||
|
"appSettings_routeWeightSuccessIncrement": "成功权重增加",
|
||||||
|
"appSettings_routeWeightSuccessIncrementSubtitle": "在成功交付后,将重量添加到路径中",
|
||||||
|
"appSettings_routeWeightFailureDecrement": "失败权重降低",
|
||||||
|
"appSettings_routeWeightFailureDecrementSubtitle": "从一条路径上移除的货物,由于无法成功交付而移除。",
|
||||||
|
"appSettings_maxMessageRetries": "最大消息重试次数",
|
||||||
|
"appSettings_maxMessageRetriesSubtitle": "在将消息标记为失败之前,允许尝试的次数",
|
||||||
|
"path_routeWeight": "{weight}/{max}",
|
||||||
|
"settings_multiAck": "多重ACK:{value}",
|
||||||
|
"settings_telemetryModeUpdated": "遥测模式已更新",
|
||||||
|
"map_showOverlaps": "重复键重叠",
|
||||||
|
"map_runTraceWithReturnPath": "沿着相同的路径返回"
|
||||||
}
|
}
|
||||||
+21
-1
@@ -4,6 +4,9 @@ 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';
|
||||||
@@ -16,6 +19,8 @@ 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 'services/chat_text_scale_service.dart';
|
||||||
|
import 'services/ui_view_state_service.dart';
|
||||||
|
import 'services/timeout_prediction_service.dart';
|
||||||
import 'storage/prefs_manager.dart';
|
import 'storage/prefs_manager.dart';
|
||||||
import 'utils/app_logger.dart';
|
import 'utils/app_logger.dart';
|
||||||
|
|
||||||
@@ -36,6 +41,8 @@ void main() async {
|
|||||||
final backgroundService = BackgroundService();
|
final backgroundService = BackgroundService();
|
||||||
final mapTileCacheService = MapTileCacheService();
|
final mapTileCacheService = MapTileCacheService();
|
||||||
final chatTextScaleService = ChatTextScaleService();
|
final chatTextScaleService = ChatTextScaleService();
|
||||||
|
final uiViewStateService = UiViewStateService();
|
||||||
|
final timeoutPredictionService = TimeoutPredictionService(storage);
|
||||||
|
|
||||||
// Load settings
|
// Load settings
|
||||||
await appSettingsService.loadSettings();
|
await appSettingsService.loadSettings();
|
||||||
@@ -53,6 +60,8 @@ void main() async {
|
|||||||
_registerThirdPartyLicenses();
|
_registerThirdPartyLicenses();
|
||||||
|
|
||||||
await chatTextScaleService.initialize();
|
await chatTextScaleService.initialize();
|
||||||
|
await uiViewStateService.initialize();
|
||||||
|
await timeoutPredictionService.initialize();
|
||||||
|
|
||||||
// Wire up connector with services
|
// Wire up connector with services
|
||||||
connector.initialize(
|
connector.initialize(
|
||||||
@@ -62,6 +71,7 @@ void main() async {
|
|||||||
bleDebugLogService: bleDebugLogService,
|
bleDebugLogService: bleDebugLogService,
|
||||||
appDebugLogService: appDebugLogService,
|
appDebugLogService: appDebugLogService,
|
||||||
backgroundService: backgroundService,
|
backgroundService: backgroundService,
|
||||||
|
timeoutPredictionService: timeoutPredictionService,
|
||||||
);
|
);
|
||||||
|
|
||||||
await connector.loadContactCache();
|
await connector.loadContactCache();
|
||||||
@@ -83,6 +93,8 @@ void main() async {
|
|||||||
appDebugLogService: appDebugLogService,
|
appDebugLogService: appDebugLogService,
|
||||||
mapTileCacheService: mapTileCacheService,
|
mapTileCacheService: mapTileCacheService,
|
||||||
chatTextScaleService: chatTextScaleService,
|
chatTextScaleService: chatTextScaleService,
|
||||||
|
uiViewStateService: uiViewStateService,
|
||||||
|
timeoutPredictionService: timeoutPredictionService,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -118,6 +130,8 @@ class MeshCoreApp extends StatelessWidget {
|
|||||||
final AppDebugLogService appDebugLogService;
|
final AppDebugLogService appDebugLogService;
|
||||||
final MapTileCacheService mapTileCacheService;
|
final MapTileCacheService mapTileCacheService;
|
||||||
final ChatTextScaleService chatTextScaleService;
|
final ChatTextScaleService chatTextScaleService;
|
||||||
|
final UiViewStateService uiViewStateService;
|
||||||
|
final TimeoutPredictionService timeoutPredictionService;
|
||||||
|
|
||||||
const MeshCoreApp({
|
const MeshCoreApp({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -130,6 +144,8 @@ class MeshCoreApp extends StatelessWidget {
|
|||||||
required this.appDebugLogService,
|
required this.appDebugLogService,
|
||||||
required this.mapTileCacheService,
|
required this.mapTileCacheService,
|
||||||
required this.chatTextScaleService,
|
required this.chatTextScaleService,
|
||||||
|
required this.uiViewStateService,
|
||||||
|
required this.timeoutPredictionService,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -143,8 +159,10 @@ class MeshCoreApp extends StatelessWidget {
|
|||||||
ChangeNotifierProvider.value(value: bleDebugLogService),
|
ChangeNotifierProvider.value(value: bleDebugLogService),
|
||||||
ChangeNotifierProvider.value(value: appDebugLogService),
|
ChangeNotifierProvider.value(value: appDebugLogService),
|
||||||
ChangeNotifierProvider.value(value: chatTextScaleService),
|
ChangeNotifierProvider.value(value: chatTextScaleService),
|
||||||
|
ChangeNotifierProvider.value(value: uiViewStateService),
|
||||||
Provider.value(value: storage),
|
Provider.value(value: storage),
|
||||||
Provider.value(value: mapTileCacheService),
|
Provider.value(value: mapTileCacheService),
|
||||||
|
ChangeNotifierProvider.value(value: timeoutPredictionService),
|
||||||
],
|
],
|
||||||
child: Consumer<AppSettingsService>(
|
child: Consumer<AppSettingsService>(
|
||||||
builder: (context, settingsService, child) {
|
builder: (context, settingsService, child) {
|
||||||
@@ -187,7 +205,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(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -18,10 +18,12 @@ class AppSettings {
|
|||||||
final bool mapShowRepeaters;
|
final bool mapShowRepeaters;
|
||||||
final bool mapShowChatNodes;
|
final bool mapShowChatNodes;
|
||||||
final bool mapShowOtherNodes;
|
final bool mapShowOtherNodes;
|
||||||
|
final bool mapShowOverlaps;
|
||||||
final double mapTimeFilterHours; // 0 = all time
|
final double mapTimeFilterHours; // 0 = all time
|
||||||
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 bool enableMessageTracing;
|
||||||
final Map<String, double>? mapCacheBounds;
|
final Map<String, double>? mapCacheBounds;
|
||||||
final int mapCacheMinZoom;
|
final int mapCacheMinZoom;
|
||||||
@@ -31,6 +33,11 @@ class AppSettings {
|
|||||||
final bool notifyOnNewChannelMessage;
|
final bool notifyOnNewChannelMessage;
|
||||||
final bool notifyOnNewAdvert;
|
final bool notifyOnNewAdvert;
|
||||||
final bool autoRouteRotationEnabled;
|
final bool autoRouteRotationEnabled;
|
||||||
|
final double maxRouteWeight;
|
||||||
|
final double initialRouteWeight;
|
||||||
|
final double routeWeightSuccessIncrement;
|
||||||
|
final double routeWeightFailureDecrement;
|
||||||
|
final int maxMessageRetries;
|
||||||
final String themeMode;
|
final String themeMode;
|
||||||
final String? languageOverride; // null = system default
|
final String? languageOverride; // null = system default
|
||||||
final bool appDebugLogEnabled;
|
final bool appDebugLogEnabled;
|
||||||
@@ -38,16 +45,21 @@ class AppSettings {
|
|||||||
final Map<String, String> batteryChemistryByRepeaterId;
|
final Map<String, String> batteryChemistryByRepeaterId;
|
||||||
final UnitSystem unitSystem;
|
final UnitSystem unitSystem;
|
||||||
final Set<String> mutedChannels;
|
final Set<String> mutedChannels;
|
||||||
|
final bool mapShowDiscoveryContacts;
|
||||||
|
final String tcpServerAddress;
|
||||||
|
final int tcpServerPort;
|
||||||
|
|
||||||
AppSettings({
|
AppSettings({
|
||||||
this.clearPathOnMaxRetry = false,
|
this.clearPathOnMaxRetry = false,
|
||||||
this.mapShowRepeaters = true,
|
this.mapShowRepeaters = true,
|
||||||
this.mapShowChatNodes = true,
|
this.mapShowChatNodes = true,
|
||||||
this.mapShowOtherNodes = true,
|
this.mapShowOtherNodes = true,
|
||||||
|
this.mapShowOverlaps = false,
|
||||||
this.mapTimeFilterHours = 0, // Default to all time
|
this.mapTimeFilterHours = 0, // Default to all time
|
||||||
this.mapKeyPrefixEnabled = false,
|
this.mapKeyPrefixEnabled = false,
|
||||||
this.mapKeyPrefix = '',
|
this.mapKeyPrefix = '',
|
||||||
this.mapShowMarkers = true,
|
this.mapShowMarkers = true,
|
||||||
|
this.mapShowGuessedLocations = true,
|
||||||
this.enableMessageTracing = false,
|
this.enableMessageTracing = false,
|
||||||
this.mapCacheBounds,
|
this.mapCacheBounds,
|
||||||
this.mapCacheMinZoom = 10,
|
this.mapCacheMinZoom = 10,
|
||||||
@@ -57,6 +69,11 @@ class AppSettings {
|
|||||||
this.notifyOnNewChannelMessage = true,
|
this.notifyOnNewChannelMessage = true,
|
||||||
this.notifyOnNewAdvert = true,
|
this.notifyOnNewAdvert = true,
|
||||||
this.autoRouteRotationEnabled = false,
|
this.autoRouteRotationEnabled = false,
|
||||||
|
this.maxRouteWeight = 5.0,
|
||||||
|
this.initialRouteWeight = 3.0,
|
||||||
|
this.routeWeightSuccessIncrement = 0.5,
|
||||||
|
this.routeWeightFailureDecrement = 0.2,
|
||||||
|
this.maxMessageRetries = 5,
|
||||||
this.themeMode = 'system',
|
this.themeMode = 'system',
|
||||||
this.languageOverride,
|
this.languageOverride,
|
||||||
this.appDebugLogEnabled = false,
|
this.appDebugLogEnabled = false,
|
||||||
@@ -64,6 +81,9 @@ class AppSettings {
|
|||||||
Map<String, String>? batteryChemistryByRepeaterId,
|
Map<String, String>? batteryChemistryByRepeaterId,
|
||||||
this.unitSystem = UnitSystem.metric,
|
this.unitSystem = UnitSystem.metric,
|
||||||
Set<String>? mutedChannels,
|
Set<String>? mutedChannels,
|
||||||
|
this.mapShowDiscoveryContacts = true,
|
||||||
|
this.tcpServerAddress = '',
|
||||||
|
this.tcpServerPort = 0,
|
||||||
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
|
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
|
||||||
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
|
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
|
||||||
mutedChannels = mutedChannels ?? {};
|
mutedChannels = mutedChannels ?? {};
|
||||||
@@ -74,10 +94,12 @@ class AppSettings {
|
|||||||
'map_show_repeaters': mapShowRepeaters,
|
'map_show_repeaters': mapShowRepeaters,
|
||||||
'map_show_chat_nodes': mapShowChatNodes,
|
'map_show_chat_nodes': mapShowChatNodes,
|
||||||
'map_show_other_nodes': mapShowOtherNodes,
|
'map_show_other_nodes': mapShowOtherNodes,
|
||||||
|
'map_show_overlaps': mapShowOverlaps,
|
||||||
'map_time_filter_hours': mapTimeFilterHours,
|
'map_time_filter_hours': mapTimeFilterHours,
|
||||||
'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,
|
'enable_message_tracing': enableMessageTracing,
|
||||||
'map_cache_bounds': mapCacheBounds,
|
'map_cache_bounds': mapCacheBounds,
|
||||||
'map_cache_min_zoom': mapCacheMinZoom,
|
'map_cache_min_zoom': mapCacheMinZoom,
|
||||||
@@ -87,6 +109,11 @@ class AppSettings {
|
|||||||
'notify_on_new_channel_message': notifyOnNewChannelMessage,
|
'notify_on_new_channel_message': notifyOnNewChannelMessage,
|
||||||
'notify_on_new_advert': notifyOnNewAdvert,
|
'notify_on_new_advert': notifyOnNewAdvert,
|
||||||
'auto_route_rotation_enabled': autoRouteRotationEnabled,
|
'auto_route_rotation_enabled': autoRouteRotationEnabled,
|
||||||
|
'max_route_weight': maxRouteWeight,
|
||||||
|
'initial_route_weight': initialRouteWeight,
|
||||||
|
'route_weight_success_increment': routeWeightSuccessIncrement,
|
||||||
|
'route_weight_failure_decrement': routeWeightFailureDecrement,
|
||||||
|
'max_message_retries': maxMessageRetries,
|
||||||
'theme_mode': themeMode,
|
'theme_mode': themeMode,
|
||||||
'language_override': languageOverride,
|
'language_override': languageOverride,
|
||||||
'app_debug_log_enabled': appDebugLogEnabled,
|
'app_debug_log_enabled': appDebugLogEnabled,
|
||||||
@@ -94,6 +121,9 @@ class AppSettings {
|
|||||||
'battery_chemistry_by_repeater_id': batteryChemistryByRepeaterId,
|
'battery_chemistry_by_repeater_id': batteryChemistryByRepeaterId,
|
||||||
'unit_system': unitSystem.value,
|
'unit_system': unitSystem.value,
|
||||||
'muted_channels': mutedChannels.toList(),
|
'muted_channels': mutedChannels.toList(),
|
||||||
|
'map_show_discovery_contacts': mapShowDiscoveryContacts,
|
||||||
|
'tcp_server_address': tcpServerAddress,
|
||||||
|
'tcp_server_port': tcpServerPort,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,11 +140,14 @@ class AppSettings {
|
|||||||
mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true,
|
mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true,
|
||||||
mapShowChatNodes: json['map_show_chat_nodes'] as bool? ?? true,
|
mapShowChatNodes: json['map_show_chat_nodes'] as bool? ?? true,
|
||||||
mapShowOtherNodes: json['map_show_other_nodes'] as bool? ?? true,
|
mapShowOtherNodes: json['map_show_other_nodes'] as bool? ?? true,
|
||||||
|
mapShowOverlaps: json['map_show_overlaps'] as bool? ?? false,
|
||||||
mapTimeFilterHours:
|
mapTimeFilterHours:
|
||||||
(json['map_time_filter_hours'] as num?)?.toDouble() ?? 0,
|
(json['map_time_filter_hours'] as num?)?.toDouble() ?? 0,
|
||||||
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,
|
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()),
|
||||||
@@ -128,6 +161,14 @@ class AppSettings {
|
|||||||
notifyOnNewAdvert: json['notify_on_new_advert'] as bool? ?? true,
|
notifyOnNewAdvert: json['notify_on_new_advert'] as bool? ?? true,
|
||||||
autoRouteRotationEnabled:
|
autoRouteRotationEnabled:
|
||||||
json['auto_route_rotation_enabled'] as bool? ?? false,
|
json['auto_route_rotation_enabled'] as bool? ?? false,
|
||||||
|
maxRouteWeight: (json['max_route_weight'] as num?)?.toDouble() ?? 5.0,
|
||||||
|
initialRouteWeight:
|
||||||
|
(json['initial_route_weight'] as num?)?.toDouble() ?? 3.0,
|
||||||
|
routeWeightSuccessIncrement:
|
||||||
|
(json['route_weight_success_increment'] as num?)?.toDouble() ?? 0.5,
|
||||||
|
routeWeightFailureDecrement:
|
||||||
|
(json['route_weight_failure_decrement'] as num?)?.toDouble() ?? 0.2,
|
||||||
|
maxMessageRetries: json['max_message_retries'] as int? ?? 5,
|
||||||
themeMode: json['theme_mode'] as String? ?? 'system',
|
themeMode: json['theme_mode'] as String? ?? 'system',
|
||||||
languageOverride: json['language_override'] as String?,
|
languageOverride: json['language_override'] as String?,
|
||||||
appDebugLogEnabled: json['app_debug_log_enabled'] as bool? ?? false,
|
appDebugLogEnabled: json['app_debug_log_enabled'] as bool? ?? false,
|
||||||
@@ -147,6 +188,10 @@ class AppSettings {
|
|||||||
?.map((e) => e.toString())
|
?.map((e) => e.toString())
|
||||||
.toSet()) ??
|
.toSet()) ??
|
||||||
{},
|
{},
|
||||||
|
mapShowDiscoveryContacts:
|
||||||
|
json['map_show_discovery_contacts'] as bool? ?? true,
|
||||||
|
tcpServerAddress: json['tcp_server_address'] as String? ?? '',
|
||||||
|
tcpServerPort: json['tcp_server_port'] as int? ?? 0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,10 +200,12 @@ class AppSettings {
|
|||||||
bool? mapShowRepeaters,
|
bool? mapShowRepeaters,
|
||||||
bool? mapShowChatNodes,
|
bool? mapShowChatNodes,
|
||||||
bool? mapShowOtherNodes,
|
bool? mapShowOtherNodes,
|
||||||
|
bool? mapShowOverlaps,
|
||||||
double? mapTimeFilterHours,
|
double? mapTimeFilterHours,
|
||||||
bool? mapKeyPrefixEnabled,
|
bool? mapKeyPrefixEnabled,
|
||||||
String? mapKeyPrefix,
|
String? mapKeyPrefix,
|
||||||
bool? mapShowMarkers,
|
bool? mapShowMarkers,
|
||||||
|
bool? mapShowGuessedLocations,
|
||||||
bool? enableMessageTracing,
|
bool? enableMessageTracing,
|
||||||
Object? mapCacheBounds = _unset,
|
Object? mapCacheBounds = _unset,
|
||||||
int? mapCacheMinZoom,
|
int? mapCacheMinZoom,
|
||||||
@@ -168,6 +215,11 @@ class AppSettings {
|
|||||||
bool? notifyOnNewChannelMessage,
|
bool? notifyOnNewChannelMessage,
|
||||||
bool? notifyOnNewAdvert,
|
bool? notifyOnNewAdvert,
|
||||||
bool? autoRouteRotationEnabled,
|
bool? autoRouteRotationEnabled,
|
||||||
|
double? maxRouteWeight,
|
||||||
|
double? initialRouteWeight,
|
||||||
|
double? routeWeightSuccessIncrement,
|
||||||
|
double? routeWeightFailureDecrement,
|
||||||
|
int? maxMessageRetries,
|
||||||
String? themeMode,
|
String? themeMode,
|
||||||
Object? languageOverride = _unset,
|
Object? languageOverride = _unset,
|
||||||
bool? appDebugLogEnabled,
|
bool? appDebugLogEnabled,
|
||||||
@@ -175,16 +227,22 @@ class AppSettings {
|
|||||||
Map<String, String>? batteryChemistryByRepeaterId,
|
Map<String, String>? batteryChemistryByRepeaterId,
|
||||||
UnitSystem? unitSystem,
|
UnitSystem? unitSystem,
|
||||||
Set<String>? mutedChannels,
|
Set<String>? mutedChannels,
|
||||||
|
bool? mapShowDiscoveryContacts,
|
||||||
|
String? tcpServerAddress,
|
||||||
|
int? tcpServerPort,
|
||||||
}) {
|
}) {
|
||||||
return AppSettings(
|
return AppSettings(
|
||||||
clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry,
|
clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry,
|
||||||
mapShowRepeaters: mapShowRepeaters ?? this.mapShowRepeaters,
|
mapShowRepeaters: mapShowRepeaters ?? this.mapShowRepeaters,
|
||||||
mapShowChatNodes: mapShowChatNodes ?? this.mapShowChatNodes,
|
mapShowChatNodes: mapShowChatNodes ?? this.mapShowChatNodes,
|
||||||
mapShowOtherNodes: mapShowOtherNodes ?? this.mapShowOtherNodes,
|
mapShowOtherNodes: mapShowOtherNodes ?? this.mapShowOtherNodes,
|
||||||
|
mapShowOverlaps: mapShowOverlaps ?? this.mapShowOverlaps,
|
||||||
mapTimeFilterHours: mapTimeFilterHours ?? this.mapTimeFilterHours,
|
mapTimeFilterHours: mapTimeFilterHours ?? this.mapTimeFilterHours,
|
||||||
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,
|
enableMessageTracing: enableMessageTracing ?? this.enableMessageTracing,
|
||||||
mapCacheBounds: mapCacheBounds == _unset
|
mapCacheBounds: mapCacheBounds == _unset
|
||||||
? this.mapCacheBounds
|
? this.mapCacheBounds
|
||||||
@@ -198,6 +256,13 @@ class AppSettings {
|
|||||||
notifyOnNewAdvert: notifyOnNewAdvert ?? this.notifyOnNewAdvert,
|
notifyOnNewAdvert: notifyOnNewAdvert ?? this.notifyOnNewAdvert,
|
||||||
autoRouteRotationEnabled:
|
autoRouteRotationEnabled:
|
||||||
autoRouteRotationEnabled ?? this.autoRouteRotationEnabled,
|
autoRouteRotationEnabled ?? this.autoRouteRotationEnabled,
|
||||||
|
maxRouteWeight: maxRouteWeight ?? this.maxRouteWeight,
|
||||||
|
initialRouteWeight: initialRouteWeight ?? this.initialRouteWeight,
|
||||||
|
routeWeightSuccessIncrement:
|
||||||
|
routeWeightSuccessIncrement ?? this.routeWeightSuccessIncrement,
|
||||||
|
routeWeightFailureDecrement:
|
||||||
|
routeWeightFailureDecrement ?? this.routeWeightFailureDecrement,
|
||||||
|
maxMessageRetries: maxMessageRetries ?? this.maxMessageRetries,
|
||||||
themeMode: themeMode ?? this.themeMode,
|
themeMode: themeMode ?? this.themeMode,
|
||||||
languageOverride: languageOverride == _unset
|
languageOverride: languageOverride == _unset
|
||||||
? this.languageOverride
|
? this.languageOverride
|
||||||
@@ -209,6 +274,10 @@ class AppSettings {
|
|||||||
batteryChemistryByRepeaterId ?? this.batteryChemistryByRepeaterId,
|
batteryChemistryByRepeaterId ?? this.batteryChemistryByRepeaterId,
|
||||||
unitSystem: unitSystem ?? this.unitSystem,
|
unitSystem: unitSystem ?? this.unitSystem,
|
||||||
mutedChannels: mutedChannels ?? this.mutedChannels,
|
mutedChannels: mutedChannels ?? this.mutedChannels,
|
||||||
|
mapShowDiscoveryContacts:
|
||||||
|
mapShowDiscoveryContacts ?? this.mapShowDiscoveryContacts,
|
||||||
|
tcpServerAddress: tcpServerAddress ?? this.tcpServerAddress,
|
||||||
|
tcpServerPort: tcpServerPort ?? this.tcpServerPort,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-8
@@ -24,20 +24,23 @@ class Channel {
|
|||||||
|
|
||||||
bool get isPublicChannel => pskHex == publicChannelPsk;
|
bool get isPublicChannel => pskHex == publicChannelPsk;
|
||||||
|
|
||||||
static Channel? fromFrame(Uint8List data) {
|
static Channel? fromFrame(Uint8List frame) {
|
||||||
// CHANNEL_INFO format:
|
// CHANNEL_INFO format:
|
||||||
// [0] = RESP_CODE_CHANNEL_INFO (18)
|
// [0] = RESP_CODE_CHANNEL_INFO (18)
|
||||||
// [1] = channel_idx
|
// [1] = channel_idx
|
||||||
// [2-33] = name (32 bytes, null-terminated)
|
// [2-33] = name (32 bytes, null-terminated)
|
||||||
// [34-49] = psk (16 bytes)
|
// [34-49] = psk (16 bytes)
|
||||||
if (data.length < 50) return null;
|
if (frame.length < 50) return null;
|
||||||
if (data[0] != respCodeChannelInfo) return null;
|
final reader = BufferReader(frame);
|
||||||
|
try {
|
||||||
final index = data[1];
|
if (reader.readByte() != respCodeChannelInfo) return null;
|
||||||
final name = readCString(data, 2, 32);
|
final index = reader.readByte();
|
||||||
final psk = Uint8List.fromList(data.sublist(34, 50));
|
final name = reader.readCStringGreedy(32);
|
||||||
|
final psk = reader.readBytes(16);
|
||||||
return Channel(index: index, name: name, psk: psk);
|
return Channel(index: index, name: name, psk: psk);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Channel empty(int index) {
|
static Channel empty(int index) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:typed_data';
|
|||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
import '../helpers/reaction_helper.dart';
|
import '../helpers/reaction_helper.dart';
|
||||||
import '../helpers/smaz.dart';
|
import '../helpers/smaz.dart';
|
||||||
|
import '../utils/app_logger.dart';
|
||||||
|
|
||||||
enum ChannelMessageStatus { pending, sent, failed }
|
enum ChannelMessageStatus { pending, sent, failed }
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ class ChannelMessage {
|
|||||||
final List<Uint8List> pathVariants;
|
final List<Uint8List> pathVariants;
|
||||||
final int? channelIndex;
|
final int? channelIndex;
|
||||||
final String messageId;
|
final String messageId;
|
||||||
|
final String? packetHash;
|
||||||
final String? replyToMessageId;
|
final String? replyToMessageId;
|
||||||
final String? replyToSenderName;
|
final String? replyToSenderName;
|
||||||
final String? replyToText;
|
final String? replyToText;
|
||||||
@@ -55,6 +57,7 @@ class ChannelMessage {
|
|||||||
List<Uint8List>? pathVariants,
|
List<Uint8List>? pathVariants,
|
||||||
this.channelIndex,
|
this.channelIndex,
|
||||||
String? messageId,
|
String? messageId,
|
||||||
|
this.packetHash,
|
||||||
this.replyToMessageId,
|
this.replyToMessageId,
|
||||||
this.replyToSenderName,
|
this.replyToSenderName,
|
||||||
this.replyToText,
|
this.replyToText,
|
||||||
@@ -79,6 +82,7 @@ class ChannelMessage {
|
|||||||
int? pathLength,
|
int? pathLength,
|
||||||
Uint8List? pathBytes,
|
Uint8List? pathBytes,
|
||||||
List<Uint8List>? pathVariants,
|
List<Uint8List>? pathVariants,
|
||||||
|
String? packetHash,
|
||||||
String? replyToMessageId,
|
String? replyToMessageId,
|
||||||
String? replyToSenderName,
|
String? replyToSenderName,
|
||||||
String? replyToText,
|
String? replyToText,
|
||||||
@@ -98,6 +102,7 @@ class ChannelMessage {
|
|||||||
pathVariants: pathVariants ?? this.pathVariants,
|
pathVariants: pathVariants ?? this.pathVariants,
|
||||||
channelIndex: channelIndex,
|
channelIndex: channelIndex,
|
||||||
messageId: messageId,
|
messageId: messageId,
|
||||||
|
packetHash: packetHash ?? this.packetHash,
|
||||||
replyToMessageId: replyToMessageId ?? this.replyToMessageId,
|
replyToMessageId: replyToMessageId ?? this.replyToMessageId,
|
||||||
replyToSenderName: replyToSenderName ?? this.replyToSenderName,
|
replyToSenderName: replyToSenderName ?? this.replyToSenderName,
|
||||||
replyToText: replyToText ?? this.replyToText,
|
replyToText: replyToText ?? this.replyToText,
|
||||||
@@ -105,58 +110,46 @@ class ChannelMessage {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static ChannelMessage? fromFrame(Uint8List data) {
|
static ChannelMessage? fromFrame(Uint8List frame) {
|
||||||
// CHANNEL_MSG_RECV format varies by version:
|
// CHANNEL_MSG_RECV format varies by version:
|
||||||
// V3: [0]=code [1]=SNR [2]=rsv1 [3]=rsv2 [4]=channel_idx [5]=path_len [path... optional] [txt_type] [timestamp x4] [text...]
|
// V3: [0]=code [1]=SNR [2]=rsv1 [3]=rsv2 [4]=channel_idx [5]=path_len [path... optional] [txt_type] [timestamp x4] [text...]
|
||||||
// Non-V3: [0]=code [1]=channel_idx [2]=path_len [3]=txt_type [4-7]=timestamp [8+]=text
|
// Non-V3: [0]=code [1]=channel_idx [2]=path_len [3]=txt_type [4-7]=timestamp [8+]=text
|
||||||
if (data.length < 8) return null;
|
if (frame.length < 8) return null;
|
||||||
|
try {
|
||||||
final code = data[0];
|
final reader = BufferReader(frame);
|
||||||
|
final code = reader.readByte();
|
||||||
if (code != respCodeChannelMsgRecv && code != respCodeChannelMsgRecvV3) {
|
if (code != respCodeChannelMsgRecv && code != respCodeChannelMsgRecvV3) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
int timestampOffset, textOffset, pathLenOffset, txtTypeOffset;
|
int pathLen;
|
||||||
|
int txtType;
|
||||||
Uint8List pathBytes = Uint8List(0);
|
Uint8List pathBytes = Uint8List(0);
|
||||||
int channelIdx;
|
int channelIdx;
|
||||||
|
|
||||||
if (code == respCodeChannelMsgRecvV3) {
|
if (code == respCodeChannelMsgRecvV3) {
|
||||||
channelIdx = data[4];
|
reader.skipBytes(1); // Skip SNR
|
||||||
pathLenOffset = 5;
|
final flags = reader.readByte();
|
||||||
final pathLen = data[pathLenOffset].toSigned(8);
|
final hasPath = (flags & 0x01) != 0;
|
||||||
var cursor = 6;
|
reader.skipBytes(1); // Skip reserved byte
|
||||||
final hasPathBytesFlag = (data[2] & 0x01) != 0;
|
channelIdx = reader.readByte();
|
||||||
final canFitPath = pathLen > 0 && data.length >= cursor + pathLen + 5;
|
pathLen = reader.readInt8();
|
||||||
final hasValidTxtType =
|
txtType = reader.readByte();
|
||||||
cursor < data.length &&
|
if (hasPath && pathLen > 0) {
|
||||||
(data[cursor] == txtTypePlain || data[cursor] == txtTypeCliData);
|
reader.rewind(); // Rewind to read path length again for pathBytes
|
||||||
if ((hasPathBytesFlag || (canFitPath && !hasValidTxtType)) &&
|
pathBytes = reader.readBytes(pathLen);
|
||||||
canFitPath) {
|
|
||||||
pathBytes = Uint8List.fromList(data.sublist(cursor, cursor + pathLen));
|
|
||||||
cursor += pathLen;
|
|
||||||
}
|
}
|
||||||
txtTypeOffset = cursor;
|
|
||||||
cursor += 1; // txt_type
|
|
||||||
timestampOffset = cursor;
|
|
||||||
textOffset = cursor + 4;
|
|
||||||
} else {
|
} else {
|
||||||
channelIdx = data[1];
|
channelIdx = reader.readByte();
|
||||||
pathLenOffset = 2;
|
pathLen = reader.readInt8();
|
||||||
txtTypeOffset = 3;
|
txtType = reader.readByte();
|
||||||
timestampOffset = 4;
|
|
||||||
textOffset = 8;
|
|
||||||
}
|
}
|
||||||
|
final timestampRaw = reader.readUInt32LE();
|
||||||
|
|
||||||
if (data.length < textOffset + 1) return null;
|
|
||||||
|
|
||||||
final txtType = data[txtTypeOffset];
|
|
||||||
if (txtType != txtTypePlain) {
|
if (txtType != txtTypePlain) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final pathLen = data[pathLenOffset].toSigned(8);
|
final text = reader.readCString();
|
||||||
final timestampRaw = readUint32LE(data, timestampOffset);
|
|
||||||
final text = readCString(data, textOffset, data.length - textOffset);
|
|
||||||
|
|
||||||
// Extract sender name and actual message from "name: msg" format
|
// Extract sender name and actual message from "name: msg" format
|
||||||
String senderName = 'Unknown';
|
String senderName = 'Unknown';
|
||||||
@@ -188,6 +181,11 @@ class ChannelMessage {
|
|||||||
pathBytes: pathBytes,
|
pathBytes: pathBytes,
|
||||||
channelIndex: channelIdx,
|
channelIndex: channelIdx,
|
||||||
);
|
);
|
||||||
|
} catch (e) {
|
||||||
|
appLogger.error('Error parsing channel message frame: $e');
|
||||||
|
// If parsing fails, return null to avoid crashes
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static ChannelMessage outgoing(
|
static ChannelMessage outgoing(
|
||||||
|
|||||||
+62
-58
@@ -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 Contact {
|
class Contact {
|
||||||
@@ -15,6 +17,9 @@ class Contact {
|
|||||||
final double? longitude;
|
final double? longitude;
|
||||||
final DateTime lastSeen;
|
final DateTime lastSeen;
|
||||||
final DateTime lastMessageAt;
|
final DateTime lastMessageAt;
|
||||||
|
final bool isActive;
|
||||||
|
final bool wasPulled;
|
||||||
|
final Uint8List? rawPacket;
|
||||||
|
|
||||||
Contact({
|
Contact({
|
||||||
required this.publicKey,
|
required this.publicKey,
|
||||||
@@ -29,6 +34,9 @@ class Contact {
|
|||||||
this.longitude,
|
this.longitude,
|
||||||
required this.lastSeen,
|
required this.lastSeen,
|
||||||
DateTime? lastMessageAt,
|
DateTime? lastMessageAt,
|
||||||
|
this.isActive = true,
|
||||||
|
this.wasPulled = false,
|
||||||
|
this.rawPacket,
|
||||||
}) : lastMessageAt = lastMessageAt ?? lastSeen;
|
}) : lastMessageAt = lastMessageAt ?? lastSeen;
|
||||||
|
|
||||||
String get publicKeyHex => pubKeyToHex(publicKey);
|
String get publicKeyHex => pubKeyToHex(publicKey);
|
||||||
@@ -59,7 +67,17 @@ class Contact {
|
|||||||
return '$pathLength hops';
|
return '$pathLength hops';
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get hasLocation => latitude != null && longitude != null;
|
bool get hasLocation {
|
||||||
|
const double epsilon = 1e-6;
|
||||||
|
final lat = latitude ?? 0.0;
|
||||||
|
final lon = longitude ?? 0.0;
|
||||||
|
return (lat.abs() > epsilon || lon.abs() > epsilon) &&
|
||||||
|
lat >= -90.0 &&
|
||||||
|
lat <= 90.0 &&
|
||||||
|
lon >= -180.0 &&
|
||||||
|
lon <= 180.0;
|
||||||
|
}
|
||||||
|
|
||||||
bool get isFavorite => (flags & contactFlagFavorite) != 0;
|
bool get isFavorite => (flags & contactFlagFavorite) != 0;
|
||||||
|
|
||||||
Contact copyWith({
|
Contact copyWith({
|
||||||
@@ -76,6 +94,8 @@ class Contact {
|
|||||||
double? longitude,
|
double? longitude,
|
||||||
DateTime? lastSeen,
|
DateTime? lastSeen,
|
||||||
DateTime? lastMessageAt,
|
DateTime? lastMessageAt,
|
||||||
|
bool? isActive,
|
||||||
|
Uint8List? rawPacket,
|
||||||
}) {
|
}) {
|
||||||
return Contact(
|
return Contact(
|
||||||
publicKey: publicKey ?? this.publicKey,
|
publicKey: publicKey ?? this.publicKey,
|
||||||
@@ -94,11 +114,13 @@ class Contact {
|
|||||||
longitude: longitude ?? this.longitude,
|
longitude: longitude ?? this.longitude,
|
||||||
lastSeen: lastSeen ?? this.lastSeen,
|
lastSeen: lastSeen ?? this.lastSeen,
|
||||||
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
|
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
|
||||||
|
isActive: isActive ?? this.isActive,
|
||||||
|
rawPacket: rawPacket ?? this.rawPacket,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String get pathIdList {
|
String get pathIdList {
|
||||||
final pathBytes = _pathBytesForDisplay;
|
final pathBytes = pathBytesForDisplay;
|
||||||
if (pathBytes.isEmpty) return '';
|
if (pathBytes.isEmpty) return '';
|
||||||
final parts = <String>[];
|
final parts = <String>[];
|
||||||
final groupSize = pathHashSize;
|
final groupSize = pathHashSize;
|
||||||
@@ -120,43 +142,7 @@ class Contact {
|
|||||||
return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>";
|
return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>";
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint8List? get traceRouteBytes {
|
Uint8List get pathBytesForDisplay {
|
||||||
final pathBytes = _pathBytesForDisplay;
|
|
||||||
Uint8List? traceBytes;
|
|
||||||
|
|
||||||
if (pathBytes.isEmpty) {
|
|
||||||
traceBytes = Uint8List(1);
|
|
||||||
traceBytes[0] = publicKey[0];
|
|
||||||
return traceBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type == advTypeRepeater || type == advTypeRoom) {
|
|
||||||
final len = (pathBytes.length + pathBytes.length + 1);
|
|
||||||
traceBytes = Uint8List(len);
|
|
||||||
traceBytes[pathBytes.length] = publicKey[0];
|
|
||||||
for (int i = 0; i < pathBytes.length; i++) {
|
|
||||||
traceBytes[i] = pathBytes[i];
|
|
||||||
if (i < pathBytes.length) {
|
|
||||||
traceBytes[len - 1 - i] = pathBytes[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (pathBytes.length < 2) {
|
|
||||||
return pathBytes[0] == 0 ? null : pathBytes;
|
|
||||||
}
|
|
||||||
final len = (pathBytes.length + pathBytes.length - 1);
|
|
||||||
traceBytes = Uint8List(len);
|
|
||||||
for (int i = 0; i < pathBytes.length; i++) {
|
|
||||||
traceBytes[i] = pathBytes[i];
|
|
||||||
if (i < pathBytes.length - 1) {
|
|
||||||
traceBytes[len - 1 - i] = pathBytes[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return traceBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
Uint8List get _pathBytesForDisplay {
|
|
||||||
if (pathOverride != null) {
|
if (pathOverride != null) {
|
||||||
if (pathOverride! < 0) return Uint8List(0);
|
if (pathOverride! < 0) return Uint8List(0);
|
||||||
return pathOverrideBytes ?? Uint8List(0);
|
return pathOverrideBytes ?? Uint8List(0);
|
||||||
@@ -166,46 +152,61 @@ class Contact {
|
|||||||
|
|
||||||
static Contact? fromFrame(Uint8List data) {
|
static Contact? fromFrame(Uint8List data) {
|
||||||
if (data.isEmpty) return null;
|
if (data.isEmpty) return null;
|
||||||
if (data[0] != respCodeContact) return null;
|
final reader = BufferReader(data);
|
||||||
try {
|
try {
|
||||||
final pubKey = Uint8List.fromList(
|
final respCode = reader.readByte();
|
||||||
data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize),
|
if (respCode != respCodeContact && respCode != pushCodeNewAdvert) {
|
||||||
);
|
return null;
|
||||||
final type = data[contactTypeOffset];
|
}
|
||||||
final flags = data[contactFlagsOffset];
|
final pubKey = reader.readBytes(pubKeySize);
|
||||||
final pathLen = data[contactPathLenOffset].toSigned(8);
|
|
||||||
|
// Guard: reject contacts with zeroed or mostly-zeroed public keys
|
||||||
|
// (indicates corrupt flash storage on the firmware side)
|
||||||
|
final zeroCount = pubKey.where((b) => b == 0).length;
|
||||||
|
if (zeroCount > pubKeySize ~/ 2) return null;
|
||||||
|
|
||||||
|
final type = reader.readByte();
|
||||||
|
final flags = reader.readByte();
|
||||||
|
final pathLen = reader.readByte();
|
||||||
final safePathLen = pathLen > 0
|
final safePathLen = pathLen > 0
|
||||||
? (pathLen > maxPathSize ? maxPathSize : pathLen)
|
? (pathLen > maxPathSize ? maxPathSize : pathLen)
|
||||||
: 0;
|
: 0;
|
||||||
final pathBytes = safePathLen > 0
|
final pathBytes = reader.readBytes(maxPathSize).sublist(0, safePathLen);
|
||||||
? Uint8List.fromList(
|
final name = reader.readCStringGreedy(maxNameSize);
|
||||||
data.sublist(contactPathOffset, contactPathOffset + safePathLen),
|
|
||||||
)
|
// Guard: reject contacts with non-printable names (corrupt flash data)
|
||||||
: Uint8List(0);
|
if (name.isNotEmpty &&
|
||||||
final name = readCString(data, contactNameOffset, maxNameSize);
|
name.codeUnits.every((c) => c < 0x20 || c == 0xFFFD)) {
|
||||||
final lastmod = readUint32LE(data, contactLastmodOffset);
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final lastMod = reader.readUInt32LE();
|
||||||
|
|
||||||
double? lat, lon;
|
double? lat, lon;
|
||||||
final latRaw = readInt32LE(data, contactLatOffset);
|
if (reader.remaining >= 8) {
|
||||||
final lonRaw = readInt32LE(data, contactLonOffset);
|
final latRaw = reader.readInt32LE();
|
||||||
|
final lonRaw = reader.readInt32LE();
|
||||||
if (latRaw != 0 || lonRaw != 0) {
|
if (latRaw != 0 || lonRaw != 0) {
|
||||||
lat = latRaw / 1e6;
|
lat = latRaw / 1e6;
|
||||||
lon = lonRaw / 1e6;
|
lon = lonRaw / 1e6;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Contact(
|
return Contact(
|
||||||
publicKey: pubKey,
|
publicKey: pubKey,
|
||||||
name: name.isEmpty ? 'Unknown' : name,
|
name: name.isEmpty ? 'Unknown' : name,
|
||||||
type: type,
|
type: type,
|
||||||
flags: flags,
|
flags: flags,
|
||||||
pathLength: pathLen,
|
pathLength: (pathLen == 0xFF || pathLen > maxPathSize) ? -1 : pathLen,
|
||||||
path: pathBytes,
|
path: pathBytes,
|
||||||
latitude: lat,
|
latitude: lat,
|
||||||
longitude: lon,
|
longitude: lon,
|
||||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000),
|
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastMod * 1000),
|
||||||
|
isActive: true,
|
||||||
|
rawPacket: null,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If parsing fails, return null
|
appLogger.error('Failed to parse contact frame: $e');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,4 +217,7 @@ class Contact {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => publicKeyHex.hashCode;
|
int get hashCode => publicKeyHex.hashCode;
|
||||||
|
bool get teleBaseEnabled => (flags & contactFlagTeleBase) != 0;
|
||||||
|
bool get teleLocEnabled => (flags & contactFlagTeleLoc) != 0;
|
||||||
|
bool get teleEnvEnabled => (flags & contactFlagTeleEnv) != 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
class DeliveryObservation {
|
||||||
|
final String contactKey;
|
||||||
|
final int pathLength;
|
||||||
|
final int messageBytes;
|
||||||
|
final int secondsSinceLastRx;
|
||||||
|
final bool isFlood;
|
||||||
|
final int deliveryMs;
|
||||||
|
final DateTime timestamp;
|
||||||
|
|
||||||
|
DeliveryObservation({
|
||||||
|
required this.contactKey,
|
||||||
|
required this.pathLength,
|
||||||
|
required this.messageBytes,
|
||||||
|
required this.secondsSinceLastRx,
|
||||||
|
required this.isFlood,
|
||||||
|
required this.deliveryMs,
|
||||||
|
required this.timestamp,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'contact_key': contactKey,
|
||||||
|
'path_length': pathLength,
|
||||||
|
'message_bytes': messageBytes,
|
||||||
|
'seconds_since_last_rx': secondsSinceLastRx,
|
||||||
|
'is_flood': isFlood,
|
||||||
|
'delivery_ms': deliveryMs,
|
||||||
|
'timestamp': timestamp.toIso8601String(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory DeliveryObservation.fromJson(Map<String, dynamic> json) {
|
||||||
|
return DeliveryObservation(
|
||||||
|
contactKey: json['contact_key'] as String,
|
||||||
|
pathLength: json['path_length'] as int,
|
||||||
|
messageBytes: json['message_bytes'] as int,
|
||||||
|
secondsSinceLastRx: json['seconds_since_last_rx'] as int? ?? 0,
|
||||||
|
isFlood: json['is_flood'] as bool,
|
||||||
|
deliveryMs: json['delivery_ms'] as int,
|
||||||
|
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
-13
@@ -16,13 +16,14 @@ class Message {
|
|||||||
final String? messageId;
|
final String? messageId;
|
||||||
final int retryCount;
|
final int retryCount;
|
||||||
final int? estimatedTimeoutMs;
|
final int? estimatedTimeoutMs;
|
||||||
final Uint8List? expectedAckHash;
|
final int? expectedAckHash;
|
||||||
final DateTime? sentAt;
|
final DateTime? sentAt;
|
||||||
final DateTime? deliveredAt;
|
final DateTime? deliveredAt;
|
||||||
final int? tripTimeMs;
|
final int? tripTimeMs;
|
||||||
final int? pathLength;
|
final int? pathLength;
|
||||||
final Uint8List pathBytes;
|
final Uint8List pathBytes;
|
||||||
final Map<String, int> reactions;
|
final Map<String, int> reactions;
|
||||||
|
final Map<String, MessageStatus> reactionStatuses;
|
||||||
final Uint8List fourByteRoomContactKey;
|
final Uint8List fourByteRoomContactKey;
|
||||||
|
|
||||||
Message({
|
Message({
|
||||||
@@ -43,9 +44,11 @@ class Message {
|
|||||||
Uint8List? pathBytes,
|
Uint8List? pathBytes,
|
||||||
Uint8List? fourByteRoomContactKey,
|
Uint8List? fourByteRoomContactKey,
|
||||||
Map<String, int>? reactions,
|
Map<String, int>? reactions,
|
||||||
|
Map<String, MessageStatus>? reactionStatuses,
|
||||||
}) : pathBytes = pathBytes ?? Uint8List(0),
|
}) : pathBytes = pathBytes ?? Uint8List(0),
|
||||||
fourByteRoomContactKey = fourByteRoomContactKey ?? Uint8List(0),
|
fourByteRoomContactKey = fourByteRoomContactKey ?? Uint8List(0),
|
||||||
reactions = reactions ?? {};
|
reactions = reactions ?? {},
|
||||||
|
reactionStatuses = reactionStatuses ?? {};
|
||||||
|
|
||||||
String get senderKeyHex => pubKeyToHex(senderKey);
|
String get senderKeyHex => pubKeyToHex(senderKey);
|
||||||
|
|
||||||
@@ -53,7 +56,7 @@ class Message {
|
|||||||
MessageStatus? status,
|
MessageStatus? status,
|
||||||
int? retryCount,
|
int? retryCount,
|
||||||
int? estimatedTimeoutMs,
|
int? estimatedTimeoutMs,
|
||||||
Uint8List? expectedAckHash,
|
int? expectedAckHash,
|
||||||
DateTime? sentAt,
|
DateTime? sentAt,
|
||||||
DateTime? deliveredAt,
|
DateTime? deliveredAt,
|
||||||
int? tripTimeMs,
|
int? tripTimeMs,
|
||||||
@@ -61,6 +64,7 @@ class Message {
|
|||||||
Uint8List? pathBytes,
|
Uint8List? pathBytes,
|
||||||
bool? isCli,
|
bool? isCli,
|
||||||
Map<String, int>? reactions,
|
Map<String, int>? reactions,
|
||||||
|
Map<String, MessageStatus>? reactionStatuses,
|
||||||
Uint8List? fourByteRoomContactKey,
|
Uint8List? fourByteRoomContactKey,
|
||||||
}) {
|
}) {
|
||||||
return Message(
|
return Message(
|
||||||
@@ -80,28 +84,28 @@ class Message {
|
|||||||
pathLength: pathLength ?? this.pathLength,
|
pathLength: pathLength ?? this.pathLength,
|
||||||
pathBytes: pathBytes ?? this.pathBytes,
|
pathBytes: pathBytes ?? this.pathBytes,
|
||||||
reactions: reactions ?? this.reactions,
|
reactions: reactions ?? this.reactions,
|
||||||
|
reactionStatuses: reactionStatuses ?? this.reactionStatuses,
|
||||||
fourByteRoomContactKey:
|
fourByteRoomContactKey:
|
||||||
fourByteRoomContactKey ?? this.fourByteRoomContactKey,
|
fourByteRoomContactKey ?? this.fourByteRoomContactKey,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Message? fromFrame(Uint8List data, Uint8List selfPubKey) {
|
static Message? fromFrame(Uint8List frame, Uint8List selfPubKey) {
|
||||||
if (data.length < msgTextOffset + 1) return null;
|
if (frame.length < msgTextOffset + 1) return null;
|
||||||
|
final reader = BufferReader(frame);
|
||||||
final code = data[0];
|
try {
|
||||||
|
final code = reader.readByte();
|
||||||
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
|
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final senderKey = Uint8List.fromList(
|
final senderKey = reader.readBytes(pubKeySize);
|
||||||
data.sublist(msgPubKeyOffset, msgPubKeyOffset + pubKeySize),
|
final timestampRaw = reader.readInt32LE();
|
||||||
);
|
final flags = reader.readByte();
|
||||||
final timestampRaw = readUint32LE(data, msgTimestampOffset);
|
|
||||||
final flags = data[msgFlagsOffset];
|
|
||||||
if ((flags >> 2) != txtTypePlain) {
|
if ((flags >> 2) != txtTypePlain) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final text = readCString(data, msgTextOffset, data.length - msgTextOffset);
|
final text = reader.readCString();
|
||||||
|
|
||||||
return Message(
|
return Message(
|
||||||
senderKey: senderKey,
|
senderKey: senderKey,
|
||||||
@@ -112,6 +116,9 @@ class Message {
|
|||||||
status: MessageStatus.delivered,
|
status: MessageStatus.delivered,
|
||||||
pathBytes: Uint8List(0),
|
pathBytes: Uint8List(0),
|
||||||
);
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Message outgoing(
|
static Message outgoing(
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
class PathRecord {
|
class PathRecord {
|
||||||
final int hopCount;
|
final int hopCount;
|
||||||
final int tripTimeMs;
|
final int tripTimeMs;
|
||||||
final DateTime timestamp;
|
final DateTime? timestamp;
|
||||||
final bool wasFloodDiscovery;
|
final bool wasFloodDiscovery;
|
||||||
final List<int> pathBytes;
|
final List<int> pathBytes;
|
||||||
final int successCount;
|
final int successCount;
|
||||||
final int failureCount;
|
final int failureCount;
|
||||||
|
final double routeWeight;
|
||||||
|
|
||||||
PathRecord({
|
PathRecord({
|
||||||
required this.hopCount,
|
required this.hopCount,
|
||||||
@@ -15,6 +16,7 @@ class PathRecord {
|
|||||||
required this.pathBytes,
|
required this.pathBytes,
|
||||||
required this.successCount,
|
required this.successCount,
|
||||||
required this.failureCount,
|
required this.failureCount,
|
||||||
|
this.routeWeight = 1.0,
|
||||||
});
|
});
|
||||||
|
|
||||||
String get displayText =>
|
String get displayText =>
|
||||||
@@ -24,11 +26,12 @@ class PathRecord {
|
|||||||
return {
|
return {
|
||||||
'hop_count': hopCount,
|
'hop_count': hopCount,
|
||||||
'trip_time_ms': tripTimeMs,
|
'trip_time_ms': tripTimeMs,
|
||||||
'timestamp': timestamp.toIso8601String(),
|
'timestamp': timestamp?.toIso8601String(),
|
||||||
'was_flood': wasFloodDiscovery,
|
'was_flood': wasFloodDiscovery,
|
||||||
'path_bytes': pathBytes,
|
'path_bytes': pathBytes,
|
||||||
'success_count': successCount,
|
'success_count': successCount,
|
||||||
'failure_count': failureCount,
|
'failure_count': failureCount,
|
||||||
|
'route_weight': routeWeight,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,12 +39,15 @@ class PathRecord {
|
|||||||
return PathRecord(
|
return PathRecord(
|
||||||
hopCount: json['hop_count'] as int,
|
hopCount: json['hop_count'] as int,
|
||||||
tripTimeMs: json['trip_time_ms'] as int,
|
tripTimeMs: json['trip_time_ms'] as int,
|
||||||
timestamp: DateTime.parse(json['timestamp'] as String),
|
timestamp: json['timestamp'] != null
|
||||||
|
? DateTime.parse(json['timestamp'] as String)
|
||||||
|
: null,
|
||||||
wasFloodDiscovery: json['was_flood'] as bool,
|
wasFloodDiscovery: json['was_flood'] as bool,
|
||||||
pathBytes:
|
pathBytes:
|
||||||
(json['path_bytes'] as List?)?.map((b) => b as int).toList() ?? [],
|
(json['path_bytes'] as List?)?.map((b) => b as int).toList() ?? [],
|
||||||
successCount: json['success_count'] as int? ?? 0,
|
successCount: json['success_count'] as int? ?? 0,
|
||||||
failureCount: json['failure_count'] as int? ?? 0,
|
failureCount: json['failure_count'] as int? ?? 0,
|
||||||
|
routeWeight: (json['route_weight'] as num?)?.toDouble() ?? 1.0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'contact.dart';
|
||||||
|
|
||||||
|
const int recentAttemptDiversityWindow = 2;
|
||||||
|
|
||||||
class PathSelection {
|
class PathSelection {
|
||||||
final List<int> pathBytes;
|
final List<int> pathBytes;
|
||||||
final int hopCount;
|
final int hopCount;
|
||||||
@@ -9,3 +15,38 @@ class PathSelection {
|
|||||||
required this.useFlood,
|
required this.useFlood,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PathSelection resolvePathSelection(
|
||||||
|
Contact contact, {
|
||||||
|
PathSelection? selection,
|
||||||
|
bool forceFlood = false,
|
||||||
|
}) {
|
||||||
|
if (contact.pathOverride != null) {
|
||||||
|
if (contact.pathOverride! < 0) {
|
||||||
|
return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true);
|
||||||
|
}
|
||||||
|
return PathSelection(
|
||||||
|
pathBytes: contact.pathOverrideBytes ?? Uint8List(0),
|
||||||
|
hopCount: contact.pathOverride!,
|
||||||
|
useFlood: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forceFlood || contact.pathLength < 0 || selection?.useFlood == true) {
|
||||||
|
return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selection != null && selection.pathBytes.isNotEmpty) {
|
||||||
|
return PathSelection(
|
||||||
|
pathBytes: selection.pathBytes,
|
||||||
|
hopCount: selection.hopCount,
|
||||||
|
useFlood: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return PathSelection(
|
||||||
|
pathBytes: contact.path,
|
||||||
|
hopCount: contact.pathLength,
|
||||||
|
useFlood: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -310,6 +310,118 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (settingsService.settings.autoRouteRotationEnabled) ...[
|
||||||
|
const Divider(height: 1),
|
||||||
|
ListTile(
|
||||||
|
title: Text(context.l10n.appSettings_maxRouteWeight),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(context.l10n.appSettings_maxRouteWeightSubtitle),
|
||||||
|
Slider(
|
||||||
|
value: settingsService.settings.maxRouteWeight,
|
||||||
|
min: 1,
|
||||||
|
max: 10,
|
||||||
|
divisions: 9,
|
||||||
|
label: settingsService.settings.maxRouteWeight
|
||||||
|
.round()
|
||||||
|
.toString(),
|
||||||
|
onChanged: (value) =>
|
||||||
|
settingsService.setMaxRouteWeight(value),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
ListTile(
|
||||||
|
title: Text(context.l10n.appSettings_initialRouteWeight),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(context.l10n.appSettings_initialRouteWeightSubtitle),
|
||||||
|
Slider(
|
||||||
|
value: settingsService.settings.initialRouteWeight,
|
||||||
|
min: 0.5,
|
||||||
|
max: 5.0,
|
||||||
|
divisions: 9,
|
||||||
|
label: settingsService.settings.initialRouteWeight
|
||||||
|
.toStringAsFixed(1),
|
||||||
|
onChanged: (value) =>
|
||||||
|
settingsService.setInitialRouteWeight(value),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
ListTile(
|
||||||
|
title: Text(context.l10n.appSettings_routeWeightSuccessIncrement),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
context
|
||||||
|
.l10n
|
||||||
|
.appSettings_routeWeightSuccessIncrementSubtitle,
|
||||||
|
),
|
||||||
|
Slider(
|
||||||
|
value: settingsService.settings.routeWeightSuccessIncrement,
|
||||||
|
min: 0.1,
|
||||||
|
max: 2.0,
|
||||||
|
divisions: 19,
|
||||||
|
label: settingsService.settings.routeWeightSuccessIncrement
|
||||||
|
.toStringAsFixed(1),
|
||||||
|
onChanged: (value) =>
|
||||||
|
settingsService.setRouteWeightSuccessIncrement(value),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
ListTile(
|
||||||
|
title: Text(context.l10n.appSettings_routeWeightFailureDecrement),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
context
|
||||||
|
.l10n
|
||||||
|
.appSettings_routeWeightFailureDecrementSubtitle,
|
||||||
|
),
|
||||||
|
Slider(
|
||||||
|
value: settingsService.settings.routeWeightFailureDecrement,
|
||||||
|
min: 0.1,
|
||||||
|
max: 2.0,
|
||||||
|
divisions: 19,
|
||||||
|
label: settingsService.settings.routeWeightFailureDecrement
|
||||||
|
.toStringAsFixed(1),
|
||||||
|
onChanged: (value) =>
|
||||||
|
settingsService.setRouteWeightFailureDecrement(value),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
ListTile(
|
||||||
|
title: Text(context.l10n.appSettings_maxMessageRetries),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(context.l10n.appSettings_maxMessageRetriesSubtitle),
|
||||||
|
Slider(
|
||||||
|
value: settingsService.settings.maxMessageRetries
|
||||||
|
.toDouble(),
|
||||||
|
min: 2,
|
||||||
|
max: 10,
|
||||||
|
divisions: 8,
|
||||||
|
label: settingsService.settings.maxMessageRetries
|
||||||
|
.toString(),
|
||||||
|
onChanged: (value) =>
|
||||||
|
settingsService.setMaxMessageRetries(value.toInt()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -118,6 +118,19 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
|||||||
: Icons.download,
|
: Icons.download,
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
|
onLongPress: () async {
|
||||||
|
await Clipboard.setData(
|
||||||
|
ClipboardData(
|
||||||
|
text: entry.payload
|
||||||
|
.map(
|
||||||
|
(b) => b
|
||||||
|
.toRadixString(16)
|
||||||
|
.padLeft(2, '0'),
|
||||||
|
)
|
||||||
|
.join(''),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,16 +283,13 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
|||||||
if (payload.length < 101) {
|
if (payload.length < 101) {
|
||||||
return 'ADVERT (short)';
|
return 'ADVERT (short)';
|
||||||
}
|
}
|
||||||
var offset = 0;
|
final reader = BufferReader(payload);
|
||||||
final pubKey = _bytesToHex(
|
try {
|
||||||
payload.sublist(offset, offset + 32),
|
final pubKey = _bytesToHex(reader.readBytes(pubKeySize), spaced: false);
|
||||||
spaced: false,
|
|
||||||
);
|
final timestamp = reader.readUInt32LE();
|
||||||
offset += 32;
|
reader.skipBytes(signatureSize);
|
||||||
final timestamp = readUint32LE(payload, offset);
|
final flags = reader.readByte();
|
||||||
offset += 4;
|
|
||||||
offset += 64; // signature
|
|
||||||
final flags = payload[offset++];
|
|
||||||
final role = _deviceRoleLabel(flags & 0x0F);
|
final role = _deviceRoleLabel(flags & 0x0F);
|
||||||
final hasLocation = (flags & 0x10) != 0;
|
final hasLocation = (flags & 0x10) != 0;
|
||||||
final hasFeature1 = (flags & 0x20) != 0;
|
final hasFeature1 = (flags & 0x20) != 0;
|
||||||
@@ -288,35 +298,35 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
|||||||
String? name;
|
String? name;
|
||||||
double? lat;
|
double? lat;
|
||||||
double? lon;
|
double? lon;
|
||||||
if (hasLocation && payload.length >= offset + 8) {
|
if (hasLocation) {
|
||||||
lat = readInt32LE(payload, offset) / 1000000.0;
|
lat = reader.readInt32LE() / 1000000.0;
|
||||||
lon = readInt32LE(payload, offset + 4) / 1000000.0;
|
lon = reader.readInt32LE() / 1000000.0;
|
||||||
offset += 8;
|
|
||||||
}
|
}
|
||||||
if (hasFeature1) offset += 2;
|
if (hasFeature1) reader.skipBytes(2);
|
||||||
if (hasFeature2) offset += 2;
|
if (hasFeature2) reader.skipBytes(2);
|
||||||
if (hasName && payload.length > offset) {
|
if (hasName) {
|
||||||
final rawName = String.fromCharCodes(payload.sublist(offset));
|
name = reader.readCStringGreedy(maxNameSize);
|
||||||
final nul = rawName.indexOf('\u0000');
|
|
||||||
name = nul >= 0 ? rawName.substring(0, nul) : rawName;
|
|
||||||
name = name.trim();
|
|
||||||
}
|
}
|
||||||
final namePart = (name != null && name.isNotEmpty) ? ' name="$name"' : '';
|
final namePart = (name != null && name.isNotEmpty) ? ' name="$name"' : '';
|
||||||
final locPart = (lat != null && lon != null)
|
final locPart = (lat != null && lon != null)
|
||||||
? ' loc=${lat.toStringAsFixed(6)},${lon.toStringAsFixed(6)}'
|
? ' loc=${lat.toStringAsFixed(6)},${lon.toStringAsFixed(6)}'
|
||||||
: '';
|
: '';
|
||||||
return 'ADVERT role=$role ts=$timestamp$namePart$locPart key=${pubKey.substring(0, 12)}…';
|
return 'ADVERT role=$role ts=$timestamp$namePart$locPart key=${pubKey.substring(0, 12)}…';
|
||||||
|
} catch (e) {
|
||||||
|
return 'ADVERT (invalid)';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _decodeControlSummary(Uint8List payload) {
|
String _decodeControlSummary(Uint8List payload) {
|
||||||
if (payload.isEmpty) return 'CONTROL (empty)';
|
final reader = BufferReader(payload);
|
||||||
final flags = payload[0];
|
try {
|
||||||
|
final flags = reader.readByte();
|
||||||
final subType = flags & 0xF0;
|
final subType = flags & 0xF0;
|
||||||
if (subType == 0x80) {
|
if (subType == 0x80) {
|
||||||
if (payload.length < 6) return 'CONTROL DISCOVER_REQ (short)';
|
if (payload.length < 6) return 'CONTROL DISCOVER_REQ (short)';
|
||||||
final typeFilter = payload[1];
|
final typeFilter = reader.readByte();
|
||||||
final tag = readUint32LE(payload, 2);
|
final tag = reader.readInt32LE();
|
||||||
final since = payload.length >= 10 ? readUint32LE(payload, 6) : 0;
|
final since = payload.length >= 10 ? reader.readInt32LE() : 0;
|
||||||
return 'CONTROL DISCOVER_REQ filter=0x${typeFilter.toRadixString(16).padLeft(2, '0')} tag=$tag since=$since';
|
return 'CONTROL DISCOVER_REQ filter=0x${typeFilter.toRadixString(16).padLeft(2, '0')} tag=$tag since=$since';
|
||||||
}
|
}
|
||||||
if (subType == 0x90) {
|
if (subType == 0x90) {
|
||||||
@@ -325,11 +335,14 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
|||||||
final snrRaw = payload[1];
|
final snrRaw = payload[1];
|
||||||
final snrSigned = snrRaw > 127 ? snrRaw - 256 : snrRaw;
|
final snrSigned = snrRaw > 127 ? snrRaw - 256 : snrRaw;
|
||||||
final snr = snrSigned / 4.0;
|
final snr = snrSigned / 4.0;
|
||||||
final tag = readUint32LE(payload, 2);
|
final tag = reader.readInt32LE();
|
||||||
final keyLen = payload.length - 6;
|
final keyLen = payload.length - 6;
|
||||||
return 'CONTROL DISCOVER_RESP node=${_deviceRoleLabel(nodeType)} snr=${snr.toStringAsFixed(2)} tag=$tag key=$keyLen';
|
return 'CONTROL DISCOVER_RESP node=${_deviceRoleLabel(nodeType)} snr=${snr.toStringAsFixed(2)} tag=$tag key=$keyLen';
|
||||||
}
|
}
|
||||||
return 'CONTROL subtype=0x${subType.toRadixString(16).padLeft(2, '0')}';
|
return 'CONTROL subtype=0x${subType.toRadixString(16).padLeft(2, '0')}';
|
||||||
|
} catch (e) {
|
||||||
|
return 'CONTROL (invalid)';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _payloadTypeLabel(int payloadType) {
|
String _payloadTypeLabel(int payloadType) {
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ 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';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
|
import '../utils/platform_info.dart';
|
||||||
import '../helpers/chat_scroll_controller.dart';
|
import '../helpers/chat_scroll_controller.dart';
|
||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
import '../helpers/link_handler.dart';
|
import '../helpers/link_handler.dart';
|
||||||
@@ -166,6 +166,33 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
|
actions: [
|
||||||
|
PopupMenuButton<String>(
|
||||||
|
icon: const Icon(Icons.more_vert),
|
||||||
|
onSelected: (value) {
|
||||||
|
if (value == 'clearChat') {
|
||||||
|
context.read<MeshCoreConnector>().clearMessagesForChannel(
|
||||||
|
widget.channel.index,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'clearChat',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.delete, size: 20, color: Colors.red),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
context.l10n.contact_clearChat,
|
||||||
|
style: const TextStyle(color: Colors.red),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
@@ -311,8 +338,13 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
],
|
],
|
||||||
Flexible(
|
Flexible(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => _showMessagePathInfo(message),
|
onTap: PlatformInfo.isDesktop
|
||||||
|
? null
|
||||||
|
: () => _showMessagePathInfo(message),
|
||||||
onLongPress: () => _showMessageActions(message),
|
onLongPress: () => _showMessageActions(message),
|
||||||
|
onSecondaryTapUp: PlatformInfo.isDesktop
|
||||||
|
? (_) => _showMessageActions(message)
|
||||||
|
: null,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: gifId != null
|
padding: gifId != null
|
||||||
? const EdgeInsets.all(4)
|
? const EdgeInsets.all(4)
|
||||||
@@ -430,7 +462,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Linkify(
|
child: LinkHandler.buildLinkifyText(
|
||||||
|
context: context,
|
||||||
text: message.text,
|
text: message.text,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: bodyFontSize * textScale,
|
fontSize: bodyFontSize * textScale,
|
||||||
@@ -440,15 +473,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
color: Colors.green,
|
color: Colors.green,
|
||||||
decoration: TextDecoration.underline,
|
decoration: TextDecoration.underline,
|
||||||
),
|
),
|
||||||
options: const LinkifyOptions(
|
|
||||||
humanize: false,
|
|
||||||
defaultToHttps: false,
|
|
||||||
),
|
|
||||||
linkifiers: const [UrlLinkifier()],
|
|
||||||
onOpen: (link) => LinkHandler.handleLinkTap(
|
|
||||||
context,
|
|
||||||
link.url,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!enableTracing && isOutgoing) ...[
|
if (!enableTracing && isOutgoing) ...[
|
||||||
@@ -557,7 +581,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isOutgoing) {
|
if (!isOutgoing && !PlatformInfo.isDesktop) {
|
||||||
return _SwipeReplyBubble(
|
return _SwipeReplyBubble(
|
||||||
maxSwipeOffset: maxSwipeOffset,
|
maxSwipeOffset: maxSwipeOffset,
|
||||||
replySwipeThreshold: replySwipeThreshold,
|
replySwipeThreshold: replySwipeThreshold,
|
||||||
@@ -1112,6 +1136,15 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
_setReplyingTo(message);
|
_setReplyingTo(message);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (PlatformInfo.isDesktop)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.route),
|
||||||
|
title: Text(context.l10n.chat_path),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(sheetContext);
|
||||||
|
_showMessagePathInfo(message);
|
||||||
|
},
|
||||||
|
),
|
||||||
// Can't react to your own messages
|
// Can't react to your own messages
|
||||||
if (!message.isOutgoing)
|
if (!message.isOutgoing)
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|||||||
@@ -40,8 +40,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
final primaryPath = !channelMessage && !message.isOutgoing
|
final primaryPath = !channelMessage && !message.isOutgoing
|
||||||
? Uint8List.fromList(primaryPathTmp.reversed.toList())
|
? Uint8List.fromList(primaryPathTmp.reversed.toList())
|
||||||
: primaryPathTmp;
|
: primaryPathTmp;
|
||||||
|
final hops = _buildPathHops(primaryPath, connector, l10n);
|
||||||
final hops = _buildPathHops(primaryPath, connector.contacts, l10n);
|
|
||||||
final hasHopDetails = primaryPath.isNotEmpty;
|
final hasHopDetails = primaryPath.isNotEmpty;
|
||||||
final observedLabel = _formatObservedHops(
|
final observedLabel = _formatObservedHops(
|
||||||
primaryPath.length,
|
primaryPath.length,
|
||||||
@@ -62,8 +61,9 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
builder: (context) => PathTraceMapScreen(
|
builder: (context) => PathTraceMapScreen(
|
||||||
title: context.l10n.contacts_repeaterPathTrace,
|
title: context.l10n.contacts_repeaterPathTrace,
|
||||||
path: primaryPath,
|
path: primaryPath,
|
||||||
flipPathRound: true,
|
flipPathAround: true,
|
||||||
reversePathRound: !message.isOutgoing && !channelMessage,
|
reversePathAround:
|
||||||
|
!(!channelMessage && !message.isOutgoing),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -302,10 +302,12 @@ class _ChannelMessagePathMapScreenState
|
|||||||
extends State<ChannelMessagePathMapScreen> {
|
extends State<ChannelMessagePathMapScreen> {
|
||||||
static const double _labelZoomThreshold = 8.5;
|
static const double _labelZoomThreshold = 8.5;
|
||||||
|
|
||||||
|
final MapController _mapController = MapController();
|
||||||
Uint8List? _selectedPath;
|
Uint8List? _selectedPath;
|
||||||
double _pathDistance = 0.0;
|
double _pathDistance = 0.0;
|
||||||
bool _showNodeLabels = true;
|
bool _showNodeLabels = true;
|
||||||
bool _didReceivePositionUpdate = false;
|
bool _didReceivePositionUpdate = false;
|
||||||
|
int? _focusedHopIndex;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -336,6 +338,22 @@ class _ChannelMessagePathMapScreenState
|
|||||||
return totalDistance;
|
return totalDistance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _focusHop(_PathHop hop) {
|
||||||
|
if (!hop.hasLocation) return;
|
||||||
|
final targetZoom = _didReceivePositionUpdate
|
||||||
|
? max(_mapController.camera.zoom, 10.0)
|
||||||
|
: 12.0;
|
||||||
|
_mapController.move(hop.position!, targetZoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onHopTapped(_PathHop hop) {
|
||||||
|
_focusHop(hop);
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_focusedHopIndex = hop.index;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<MeshCoreConnector>(
|
return Consumer<MeshCoreConnector>(
|
||||||
@@ -364,11 +382,7 @@ class _ChannelMessagePathMapScreenState
|
|||||||
: selectedPathTmp;
|
: selectedPathTmp;
|
||||||
|
|
||||||
final selectedIndex = _indexForPath(selectedPath, observedPaths);
|
final selectedIndex = _indexForPath(selectedPath, observedPaths);
|
||||||
final hops = _buildPathHops(
|
final hops = _buildPathHops(selectedPath, connector, context.l10n);
|
||||||
selectedPath,
|
|
||||||
connector.contacts,
|
|
||||||
context.l10n,
|
|
||||||
);
|
|
||||||
|
|
||||||
final points = <LatLng>[];
|
final points = <LatLng>[];
|
||||||
|
|
||||||
@@ -423,6 +437,7 @@ class _ChannelMessagePathMapScreenState
|
|||||||
children: [
|
children: [
|
||||||
FlutterMap(
|
FlutterMap(
|
||||||
key: mapKey,
|
key: mapKey,
|
||||||
|
mapController: _mapController,
|
||||||
options: MapOptions(
|
options: MapOptions(
|
||||||
initialCenter: initialCenter,
|
initialCenter: initialCenter,
|
||||||
initialZoom: initialZoom,
|
initialZoom: initialZoom,
|
||||||
@@ -474,6 +489,7 @@ class _ChannelMessagePathMapScreenState
|
|||||||
) {
|
) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedPath = observedPaths[index].pathBytes;
|
_selectedPath = observedPaths[index].pathBytes;
|
||||||
|
_focusedHopIndex = null;
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
if (points.isEmpty)
|
if (points.isEmpty)
|
||||||
@@ -729,8 +745,17 @@ class _ChannelMessagePathMapScreenState
|
|||||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final hop = hops[index];
|
final hop = hops[index];
|
||||||
|
final isFocused = _focusedHopIndex == hop.index;
|
||||||
return ListTile(
|
return ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
|
enabled: hop.hasLocation,
|
||||||
|
selected: isFocused,
|
||||||
|
selectedTileColor: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primary.withValues(alpha: 0.12),
|
||||||
|
onTap: hop.hasLocation
|
||||||
|
? () => _onHopTapped(hop)
|
||||||
|
: null,
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
radius: 14,
|
radius: 14,
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -789,19 +814,71 @@ class _ObservedPath {
|
|||||||
|
|
||||||
List<_PathHop> _buildPathHops(
|
List<_PathHop> _buildPathHops(
|
||||||
Uint8List pathBytes,
|
Uint8List pathBytes,
|
||||||
List<Contact> contacts,
|
MeshCoreConnector connector,
|
||||||
AppLocalizations l10n,
|
AppLocalizations l10n,
|
||||||
) {
|
) {
|
||||||
|
if (pathBytes.isEmpty) return const [];
|
||||||
|
final candidatesByPrefix = <int, List<Contact>>{};
|
||||||
|
for (final contact in connector.allContacts) {
|
||||||
|
if (contact.publicKey.isEmpty) continue;
|
||||||
|
if (contact.type != advTypeRepeater && contact.type != advTypeRoom) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final prefix = contact.publicKey.first;
|
||||||
|
candidatesByPrefix.putIfAbsent(prefix, () => <Contact>[]).add(contact);
|
||||||
|
}
|
||||||
|
for (final candidates in candidatesByPrefix.values) {
|
||||||
|
candidates.sort((a, b) => b.lastSeen.compareTo(a.lastSeen));
|
||||||
|
}
|
||||||
|
final startPoint =
|
||||||
|
(connector.selfLatitude != null && connector.selfLongitude != null)
|
||||||
|
? LatLng(connector.selfLatitude!, connector.selfLongitude!)
|
||||||
|
: null;
|
||||||
|
var previousPosition = startPoint;
|
||||||
|
final distance = Distance();
|
||||||
|
|
||||||
final hops = <_PathHop>[];
|
final hops = <_PathHop>[];
|
||||||
for (var i = 0; i < pathBytes.length; i++) {
|
for (var i = 0; i < pathBytes.length; i++) {
|
||||||
final prefix = pathBytes[i];
|
final searchPoint = i == 0 ? startPoint : previousPosition;
|
||||||
final contact = _matchContactForPrefix(contacts, prefix);
|
final candidates = candidatesByPrefix[pathBytes[i]];
|
||||||
|
Contact? contact;
|
||||||
|
if (candidates != null && candidates.isNotEmpty) {
|
||||||
|
var bestIndex = 0;
|
||||||
|
if (searchPoint != null) {
|
||||||
|
var bestDistance = double.infinity;
|
||||||
|
for (var j = 0; j < candidates.length; j++) {
|
||||||
|
final candidate = candidates[j];
|
||||||
|
if (!candidate.hasLocation ||
|
||||||
|
candidate.latitude == null ||
|
||||||
|
candidate.longitude == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final currentDistance = distance(
|
||||||
|
searchPoint,
|
||||||
|
LatLng(candidate.latitude!, candidate.longitude!),
|
||||||
|
);
|
||||||
|
if (currentDistance < bestDistance) {
|
||||||
|
bestDistance = currentDistance;
|
||||||
|
bestIndex = j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contact = candidates.removeAt(bestIndex);
|
||||||
|
if (candidates.isEmpty) {
|
||||||
|
candidatesByPrefix.remove(pathBytes[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final resolvedPosition = _resolvePosition(contact);
|
||||||
|
if (resolvedPosition != null) {
|
||||||
|
previousPosition = resolvedPosition;
|
||||||
|
}
|
||||||
hops.add(
|
hops.add(
|
||||||
_PathHop(
|
_PathHop(
|
||||||
index: i + 1,
|
index: i + 1,
|
||||||
prefix: prefix,
|
prefix: pathBytes[i],
|
||||||
contact: contact,
|
contact: contact,
|
||||||
position: _resolvePosition(contact),
|
position: resolvedPosition,
|
||||||
l10n: l10n,
|
l10n: l10n,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -809,42 +886,13 @@ List<_PathHop> _buildPathHops(
|
|||||||
return hops;
|
return hops;
|
||||||
}
|
}
|
||||||
|
|
||||||
Contact? _matchContactForPrefix(List<Contact> contacts, int prefix) {
|
|
||||||
final matches = contacts
|
|
||||||
.where(
|
|
||||||
(contact) =>
|
|
||||||
(contact.type == advTypeRepeater || contact.type == advTypeRoom) &&
|
|
||||||
contact.publicKey.isNotEmpty &&
|
|
||||||
contact.publicKey[0] == prefix,
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
if (matches.isEmpty) return null;
|
|
||||||
|
|
||||||
Contact? pickWhere(bool Function(Contact) predicate) {
|
|
||||||
for (final contact in matches) {
|
|
||||||
if (predicate(contact)) return contact;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return pickWhere((c) => c.type == advTypeRepeater && _hasValidLocation(c)) ??
|
|
||||||
pickWhere((c) => c.type == advTypeRepeater) ??
|
|
||||||
pickWhere(_hasValidLocation) ??
|
|
||||||
matches.first;
|
|
||||||
}
|
|
||||||
|
|
||||||
LatLng? _resolvePosition(Contact? contact) {
|
LatLng? _resolvePosition(Contact? contact) {
|
||||||
if (contact == null) return null;
|
if (contact == null) return null;
|
||||||
if (!_hasValidLocation(contact)) return null;
|
if (!contact.hasLocation) return null;
|
||||||
return LatLng(contact.latitude!, contact.longitude!);
|
final latitude = contact.latitude;
|
||||||
}
|
final longitude = contact.longitude;
|
||||||
|
if (latitude == null || longitude == null) return null;
|
||||||
bool _hasValidLocation(Contact contact) {
|
return LatLng(latitude, longitude);
|
||||||
final lat = contact.latitude;
|
|
||||||
final lon = contact.longitude;
|
|
||||||
if (lat == null || lon == null) return false;
|
|
||||||
if (lat == 0 && lon == 0) return false;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatPrefix(int prefix) {
|
String _formatPrefix(int prefix) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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/storage/channel_message_store.dart';
|
||||||
|
import 'package:meshcore_open/utils/platform_info.dart';
|
||||||
import 'package:meshcore_open/widgets/app_bar.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';
|
||||||
@@ -11,6 +12,7 @@ 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 '../services/app_settings_service.dart';
|
||||||
|
import '../services/ui_view_state_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';
|
||||||
@@ -28,8 +30,6 @@ import 'contacts_screen.dart';
|
|||||||
import 'map_screen.dart';
|
import 'map_screen.dart';
|
||||||
import 'settings_screen.dart';
|
import 'settings_screen.dart';
|
||||||
|
|
||||||
enum ChannelSortOption { manual, name, latestMessages, unread }
|
|
||||||
|
|
||||||
class ChannelsScreen extends StatefulWidget {
|
class ChannelsScreen extends StatefulWidget {
|
||||||
final bool hideBackButton;
|
final bool hideBackButton;
|
||||||
|
|
||||||
@@ -43,17 +43,20 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
with DisconnectNavigationMixin {
|
with DisconnectNavigationMixin {
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
final CommunityStore _communityStore = CommunityStore();
|
final CommunityStore _communityStore = CommunityStore();
|
||||||
String _searchQuery = '';
|
|
||||||
Timer? _searchDebounce;
|
Timer? _searchDebounce;
|
||||||
ChannelSortOption _sortOption = ChannelSortOption.manual;
|
|
||||||
List<Community> _communities = [];
|
List<Community> _communities = [];
|
||||||
|
|
||||||
// Cache of PSK hex -> Community for quick lookup
|
// Cache of PSK hex -> Community for quick lookup
|
||||||
final Map<String, Community> _pskToCommunity = {};
|
final Map<String, Community> _pskToCommunity = {};
|
||||||
|
|
||||||
|
ChannelMessageStore get _channelMessageStore => ChannelMessageStore();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_searchController.text = context
|
||||||
|
.read<UiViewStateService>()
|
||||||
|
.channelsSearchText;
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
context.read<MeshCoreConnector>().getChannels();
|
context.read<MeshCoreConnector>().getChannels();
|
||||||
_loadCommunities();
|
_loadCommunities();
|
||||||
@@ -61,6 +64,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadCommunities() async {
|
Future<void> _loadCommunities() async {
|
||||||
|
final connector = context.read<MeshCoreConnector>();
|
||||||
|
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||||
final communities = await _communityStore.loadCommunities();
|
final communities = await _communityStore.loadCommunities();
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -106,7 +111,10 @@ 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 viewState = context.watch<UiViewStateService>();
|
||||||
|
|
||||||
final channelMessageStore = ChannelMessageStore();
|
final channelMessageStore = ChannelMessageStore();
|
||||||
|
channelMessageStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||||
|
|
||||||
// Auto-navigate back to scanner if disconnected
|
// Auto-navigate back to scanner if disconnected
|
||||||
if (!checkConnectionAndNavigate(connector)) {
|
if (!checkConnectionAndNavigate(connector)) {
|
||||||
@@ -199,6 +207,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
final filteredChannels = _filterAndSortChannels(
|
final filteredChannels = _filterAndSortChannels(
|
||||||
channels,
|
channels,
|
||||||
connector,
|
connector,
|
||||||
|
viewState,
|
||||||
);
|
);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
@@ -213,17 +222,19 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
suffixIcon: Row(
|
suffixIcon: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (_searchQuery.isNotEmpty)
|
if (viewState.channelsSearchText.isNotEmpty)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.clear),
|
icon: const Icon(Icons.clear),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
_searchDebounce?.cancel();
|
||||||
|
_searchDebounce = null;
|
||||||
_searchController.clear();
|
_searchController.clear();
|
||||||
setState(() {
|
context
|
||||||
_searchQuery = '';
|
.read<UiViewStateService>()
|
||||||
});
|
.setChannelsSearchText('');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_buildFilterButton(),
|
_buildFilterButton(viewState),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
@@ -240,9 +251,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
const Duration(milliseconds: 300),
|
const Duration(milliseconds: 300),
|
||||||
() {
|
() {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
context
|
||||||
_searchQuery = value.toLowerCase();
|
.read<UiViewStateService>()
|
||||||
});
|
.setChannelsSearchText(value);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -277,8 +288,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: (_sortOption == ChannelSortOption.manual &&
|
: (viewState.channelsSortOption ==
|
||||||
_searchQuery.isEmpty)
|
ChannelSortOption.manual &&
|
||||||
|
viewState.channelsSearchText.isEmpty)
|
||||||
? ReorderableListView.builder(
|
? ReorderableListView.builder(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
left: 16,
|
left: 16,
|
||||||
@@ -406,6 +418,15 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
return Card(
|
return Card(
|
||||||
key: ValueKey('channel_${channel.index}'),
|
key: ValueKey('channel_${channel.index}'),
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: GestureDetector(
|
||||||
|
onSecondaryTapUp: PlatformInfo.isDesktop
|
||||||
|
? (_) => _showChannelActions(
|
||||||
|
context,
|
||||||
|
connector,
|
||||||
|
channelMessageStore,
|
||||||
|
channel,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
minVerticalPadding: 0,
|
minVerticalPadding: 0,
|
||||||
@@ -432,7 +453,11 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.people, size: 8, color: Colors.white),
|
child: const Icon(
|
||||||
|
Icons.people,
|
||||||
|
size: 8,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -443,7 +468,11 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
: channel.name,
|
: channel.name,
|
||||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||||
),
|
),
|
||||||
subtitle: Text(subtitle, maxLines: 1, overflow: TextOverflow.ellipsis),
|
subtitle: Text(
|
||||||
|
subtitle,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -480,6 +509,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
channel,
|
channel,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,59 +608,40 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
await showDisconnectDialog(context, connector);
|
await showDisconnectDialog(context, connector);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFilterButton() {
|
Widget _buildFilterButton(UiViewStateService viewState) {
|
||||||
const actionSortManual = 0;
|
return SortFilterMenu<ChannelSortOption>(
|
||||||
const actionSortName = 1;
|
|
||||||
const actionSortLatest = 2;
|
|
||||||
const actionSortUnread = 3;
|
|
||||||
|
|
||||||
return SortFilterMenu(
|
|
||||||
tooltip: context.l10n.listFilter_tooltip,
|
tooltip: context.l10n.listFilter_tooltip,
|
||||||
sections: [
|
sections: [
|
||||||
SortFilterMenuSection(
|
SortFilterMenuSection<ChannelSortOption>(
|
||||||
title: context.l10n.channels_sortBy,
|
title: context.l10n.channels_sortBy,
|
||||||
options: [
|
options: [
|
||||||
SortFilterMenuOption(
|
SortFilterMenuOption<ChannelSortOption>(
|
||||||
value: actionSortManual,
|
value: ChannelSortOption.manual,
|
||||||
label: context.l10n.channels_sortManual,
|
label: context.l10n.channels_sortManual,
|
||||||
checked: _sortOption == ChannelSortOption.manual,
|
checked: viewState.channelsSortOption == ChannelSortOption.manual,
|
||||||
),
|
),
|
||||||
SortFilterMenuOption(
|
SortFilterMenuOption<ChannelSortOption>(
|
||||||
value: actionSortName,
|
value: ChannelSortOption.name,
|
||||||
label: context.l10n.channels_sortAZ,
|
label: context.l10n.channels_sortAZ,
|
||||||
checked: _sortOption == ChannelSortOption.name,
|
checked: viewState.channelsSortOption == ChannelSortOption.name,
|
||||||
),
|
),
|
||||||
SortFilterMenuOption(
|
SortFilterMenuOption<ChannelSortOption>(
|
||||||
value: actionSortLatest,
|
value: ChannelSortOption.latestMessages,
|
||||||
label: context.l10n.channels_sortLatestMessages,
|
label: context.l10n.channels_sortLatestMessages,
|
||||||
checked: _sortOption == ChannelSortOption.latestMessages,
|
checked:
|
||||||
|
viewState.channelsSortOption ==
|
||||||
|
ChannelSortOption.latestMessages,
|
||||||
),
|
),
|
||||||
SortFilterMenuOption(
|
SortFilterMenuOption<ChannelSortOption>(
|
||||||
value: actionSortUnread,
|
value: ChannelSortOption.unread,
|
||||||
label: context.l10n.channels_sortUnread,
|
label: context.l10n.channels_sortUnread,
|
||||||
checked: _sortOption == ChannelSortOption.unread,
|
checked: viewState.channelsSortOption == ChannelSortOption.unread,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
onSelected: (action) {
|
onSelected: (sortOption) {
|
||||||
setState(() {
|
viewState.setChannelsSortOption(sortOption);
|
||||||
switch (action) {
|
|
||||||
case actionSortManual:
|
|
||||||
_sortOption = ChannelSortOption.manual;
|
|
||||||
break;
|
|
||||||
case actionSortLatest:
|
|
||||||
_sortOption = ChannelSortOption.latestMessages;
|
|
||||||
break;
|
|
||||||
case actionSortUnread:
|
|
||||||
_sortOption = ChannelSortOption.unread;
|
|
||||||
break;
|
|
||||||
case actionSortName:
|
|
||||||
default:
|
|
||||||
_sortOption = ChannelSortOption.name;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -638,11 +649,14 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
List<Channel> _filterAndSortChannels(
|
List<Channel> _filterAndSortChannels(
|
||||||
List<Channel> channels,
|
List<Channel> channels,
|
||||||
MeshCoreConnector connector,
|
MeshCoreConnector connector,
|
||||||
|
UiViewStateService viewState,
|
||||||
) {
|
) {
|
||||||
var filtered = channels.where((channel) {
|
var filtered = channels.where((channel) {
|
||||||
if (_searchQuery.isEmpty) return true;
|
if (viewState.channelsSearchText.isEmpty) return true;
|
||||||
final label = _normalizeChannelName(channel);
|
final label = _normalizeChannelName(channel);
|
||||||
return label.toLowerCase().contains(_searchQuery);
|
return label.toLowerCase().contains(
|
||||||
|
viewState.channelsSearchText.toLowerCase(),
|
||||||
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
int compareByName(Channel a, Channel b) {
|
int compareByName(Channel a, Channel b) {
|
||||||
@@ -651,7 +665,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
return nameA.toLowerCase().compareTo(nameB.toLowerCase());
|
return nameA.toLowerCase().compareTo(nameB.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (_sortOption) {
|
switch (viewState.channelsSortOption) {
|
||||||
case ChannelSortOption.manual:
|
case ChannelSortOption.manual:
|
||||||
break;
|
break;
|
||||||
case ChannelSortOption.latestMessages:
|
case ChannelSortOption.latestMessages:
|
||||||
@@ -712,6 +726,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
bool isRegularHashtag = true;
|
bool isRegularHashtag = true;
|
||||||
Community? selectedCommunity;
|
Community? selectedCommunity;
|
||||||
|
|
||||||
|
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (dialogContext) => StatefulBuilder(
|
builder: (dialogContext) => StatefulBuilder(
|
||||||
@@ -763,7 +779,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget? buildExpandedContent() {
|
Widget? buildExpandedContent(
|
||||||
|
ChannelMessageStore channelMessageStore,
|
||||||
|
) {
|
||||||
switch (selectedOption) {
|
switch (selectedOption) {
|
||||||
case 0: // Create Private Channel
|
case 0: // Create Private Channel
|
||||||
return Column(
|
return Column(
|
||||||
@@ -788,7 +806,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: FilledButton(
|
child: FilledButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
final name = nameController.text.trim();
|
final name = nameController.text.trim();
|
||||||
if (name.isEmpty) {
|
if (name.isEmpty) {
|
||||||
ScaffoldMessenger.of(
|
ScaffoldMessenger.of(
|
||||||
@@ -810,7 +828,14 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
psk[i] = random.nextInt(256);
|
psk[i] = random.nextInt(256);
|
||||||
}
|
}
|
||||||
Navigator.pop(dialogContext);
|
Navigator.pop(dialogContext);
|
||||||
connector.setChannel(nextIndex, name, psk);
|
await connector.setChannel(
|
||||||
|
nextIndex,
|
||||||
|
name,
|
||||||
|
psk,
|
||||||
|
);
|
||||||
|
await channelMessageStore.clearChannelMessages(
|
||||||
|
nextIndex,
|
||||||
|
);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
@@ -1329,7 +1354,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
subtitle:
|
subtitle:
|
||||||
dialogContext.l10n.channels_createPrivateChannelDesc,
|
dialogContext.l10n.channels_createPrivateChannelDesc,
|
||||||
),
|
),
|
||||||
if (selectedOption == 0) buildExpandedContent()!,
|
if (selectedOption == 0)
|
||||||
|
buildExpandedContent(_channelMessageStore)!,
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
buildOptionTile(
|
buildOptionTile(
|
||||||
optionIndex: 1,
|
optionIndex: 1,
|
||||||
@@ -1338,7 +1364,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
subtitle:
|
subtitle:
|
||||||
dialogContext.l10n.channels_joinPrivateChannelDesc,
|
dialogContext.l10n.channels_joinPrivateChannelDesc,
|
||||||
),
|
),
|
||||||
if (selectedOption == 1) buildExpandedContent()!,
|
if (selectedOption == 1)
|
||||||
|
buildExpandedContent(_channelMessageStore)!,
|
||||||
if (!hasPublicChannel) ...[
|
if (!hasPublicChannel) ...[
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
buildOptionTile(
|
buildOptionTile(
|
||||||
@@ -1348,7 +1375,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
subtitle:
|
subtitle:
|
||||||
dialogContext.l10n.channels_joinPublicChannelDesc,
|
dialogContext.l10n.channels_joinPublicChannelDesc,
|
||||||
),
|
),
|
||||||
if (selectedOption == 2) buildExpandedContent()!,
|
if (selectedOption == 2)
|
||||||
|
buildExpandedContent(_channelMessageStore)!,
|
||||||
],
|
],
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
buildOptionTile(
|
buildOptionTile(
|
||||||
@@ -1358,7 +1386,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
subtitle:
|
subtitle:
|
||||||
dialogContext.l10n.channels_joinHashtagChannelDesc,
|
dialogContext.l10n.channels_joinHashtagChannelDesc,
|
||||||
),
|
),
|
||||||
if (selectedOption == 3) buildExpandedContent()!,
|
if (selectedOption == 3)
|
||||||
|
buildExpandedContent(_channelMessageStore)!,
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
buildOptionTile(
|
buildOptionTile(
|
||||||
optionIndex: 4,
|
optionIndex: 4,
|
||||||
@@ -1366,7 +1395,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
title: dialogContext.l10n.community_scanQr,
|
title: dialogContext.l10n.community_scanQr,
|
||||||
subtitle: dialogContext.l10n.community_join,
|
subtitle: dialogContext.l10n.community_join,
|
||||||
),
|
),
|
||||||
if (selectedOption == 4) buildExpandedContent()!,
|
if (selectedOption == 4)
|
||||||
|
buildExpandedContent(_channelMessageStore)!,
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
buildOptionTile(
|
buildOptionTile(
|
||||||
optionIndex: 5,
|
optionIndex: 5,
|
||||||
@@ -1374,7 +1404,8 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
title: dialogContext.l10n.community_create,
|
title: dialogContext.l10n.community_create,
|
||||||
subtitle: dialogContext.l10n.community_createDesc,
|
subtitle: dialogContext.l10n.community_createDesc,
|
||||||
),
|
),
|
||||||
if (selectedOption == 5) buildExpandedContent()!,
|
if (selectedOption == 5)
|
||||||
|
buildExpandedContent(_channelMessageStore)!,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1524,7 +1555,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
try {
|
try {
|
||||||
await connector.deleteChannel(channel.index);
|
await connector.deleteChannel(channel.index);
|
||||||
|
|
||||||
channelMessageStore.clearChannelMessages(channel.index);
|
await channelMessageStore.clearChannelMessages(channel.index);
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
|
|
||||||
@@ -1749,6 +1780,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
final channelCount = communityChannels.length;
|
final channelCount = communityChannels.length;
|
||||||
|
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
|||||||
+347
-76
@@ -5,9 +5,10 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
|
||||||
import 'package:meshcore_open/screens/path_trace_map.dart';
|
import 'package:meshcore_open/screens/path_trace_map.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../utils/platform_info.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
@@ -16,6 +17,7 @@ import '../helpers/reaction_helper.dart';
|
|||||||
import '../widgets/message_status_icon.dart';
|
import '../widgets/message_status_icon.dart';
|
||||||
import '../helpers/chat_scroll_controller.dart';
|
import '../helpers/chat_scroll_controller.dart';
|
||||||
import '../helpers/link_handler.dart';
|
import '../helpers/link_handler.dart';
|
||||||
|
import '../helpers/path_helper.dart';
|
||||||
import '../helpers/utf8_length_limiter.dart';
|
import '../helpers/utf8_length_limiter.dart';
|
||||||
import '../models/channel_message.dart';
|
import '../models/channel_message.dart';
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
@@ -36,6 +38,7 @@ import '../widgets/gif_picker.dart';
|
|||||||
import '../widgets/path_selection_dialog.dart';
|
import '../widgets/path_selection_dialog.dart';
|
||||||
import '../utils/app_logger.dart';
|
import '../utils/app_logger.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
|
import 'telemetry_screen.dart';
|
||||||
|
|
||||||
class ChatScreen extends StatefulWidget {
|
class ChatScreen extends StatefulWidget {
|
||||||
final Contact contact;
|
final Contact contact;
|
||||||
@@ -106,10 +109,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
final unreadLabel = context.l10n.chat_unread(unreadCount);
|
final unreadLabel = context.l10n.chat_unread(unreadCount);
|
||||||
final pathLabel = _currentPathLabel(contact);
|
final pathLabel = _currentPathLabel(contact);
|
||||||
|
|
||||||
// Show path details if we have path data (from device or override)
|
// Show path details if we have non-empty path data (from device or override)
|
||||||
final hasPathData =
|
|
||||||
contact.path.isNotEmpty || contact.pathOverrideBytes != null;
|
|
||||||
final effectivePath = contact.pathOverrideBytes ?? contact.path;
|
final effectivePath = contact.pathOverrideBytes ?? contact.path;
|
||||||
|
final hasPathData = effectivePath.isNotEmpty;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -143,12 +145,25 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
final contact = _resolveContact(connector);
|
final contact = _resolveContact(connector);
|
||||||
final isFloodMode = contact.pathOverride == -1;
|
final isFloodMode = contact.pathOverride == -1;
|
||||||
|
|
||||||
|
final isDirectMode = contact.pathOverride == 0;
|
||||||
|
final activeMode = isFloodMode
|
||||||
|
? 'flood'
|
||||||
|
: isDirectMode
|
||||||
|
? 'direct'
|
||||||
|
: 'auto';
|
||||||
|
|
||||||
return PopupMenuButton<String>(
|
return PopupMenuButton<String>(
|
||||||
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
|
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
|
||||||
tooltip: context.l10n.chat_routingMode,
|
tooltip: context.l10n.chat_routingMode,
|
||||||
onSelected: (mode) async {
|
onSelected: (mode) async {
|
||||||
if (mode == 'flood') {
|
if (mode == 'flood') {
|
||||||
await connector.setPathOverride(contact, pathLen: -1);
|
await connector.setPathOverride(contact, pathLen: -1);
|
||||||
|
} else if (mode == 'direct') {
|
||||||
|
await connector.setPathOverride(
|
||||||
|
contact,
|
||||||
|
pathLen: 0,
|
||||||
|
pathBytes: Uint8List(0),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
await connector.setPathOverride(contact, pathLen: null);
|
await connector.setPathOverride(contact, pathLen: null);
|
||||||
}
|
}
|
||||||
@@ -161,7 +176,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.auto_mode,
|
Icons.auto_mode,
|
||||||
size: 20,
|
size: 20,
|
||||||
color: !isFloodMode
|
color: activeMode == 'auto'
|
||||||
? Theme.of(context).primaryColor
|
? Theme.of(context).primaryColor
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@@ -169,7 +184,30 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
Text(
|
Text(
|
||||||
context.l10n.chat_autoUseSavedPath,
|
context.l10n.chat_autoUseSavedPath,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: !isFloodMode
|
fontWeight: activeMode == 'auto'
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'direct',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.near_me,
|
||||||
|
size: 20,
|
||||||
|
color: activeMode == 'direct'
|
||||||
|
? Theme.of(context).primaryColor
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
context.l10n.chat_direct,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: activeMode == 'direct'
|
||||||
? FontWeight.bold
|
? FontWeight.bold
|
||||||
: FontWeight.normal,
|
: FontWeight.normal,
|
||||||
),
|
),
|
||||||
@@ -184,7 +222,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.waves,
|
Icons.waves,
|
||||||
size: 20,
|
size: 20,
|
||||||
color: isFloodMode
|
color: activeMode == 'flood'
|
||||||
? Theme.of(context).primaryColor
|
? Theme.of(context).primaryColor
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@@ -192,7 +230,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
Text(
|
Text(
|
||||||
context.l10n.chat_forceFloodMode,
|
context.l10n.chat_forceFloodMode,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: isFloodMode
|
fontWeight: activeMode == 'flood'
|
||||||
? FontWeight.bold
|
? FontWeight.bold
|
||||||
: FontWeight.normal,
|
: FontWeight.normal,
|
||||||
),
|
),
|
||||||
@@ -209,9 +247,77 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
tooltip: context.l10n.chat_pathManagement,
|
tooltip: context.l10n.chat_pathManagement,
|
||||||
onPressed: () => _showPathHistory(context),
|
onPressed: () => _showPathHistory(context),
|
||||||
),
|
),
|
||||||
IconButton(
|
Consumer<MeshCoreConnector>(
|
||||||
icon: const Icon(Icons.info_outline),
|
builder: (context, connector, _) {
|
||||||
onPressed: () => _showContactInfo(context),
|
return PopupMenuButton<String>(
|
||||||
|
icon: const Icon(Icons.more_vert),
|
||||||
|
onSelected: (value) {
|
||||||
|
if (value == 'info') {
|
||||||
|
_showContactInfo(context);
|
||||||
|
}
|
||||||
|
if (value == 'settings') {
|
||||||
|
_showContactSettings(context);
|
||||||
|
}
|
||||||
|
if (value == 'telemetry') {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
TelemetryScreen(contact: widget.contact),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (value == 'clearChat') {
|
||||||
|
connector.clearMessagesForContact(widget.contact);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'info',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.info_outline, size: 20),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(context.l10n.contact_info),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'telemetry',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.bar_chart, size: 20),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(context.l10n.contact_telemetry),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'settings',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.settings, size: 20),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(context.l10n.contact_settings),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'clearChat',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.delete, size: 20, color: Colors.red),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
context.l10n.contact_clearChat,
|
||||||
|
style: const TextStyle(color: Colors.red),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -251,7 +357,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
context.l10n.chat_sendMessageTo(widget.contact.name),
|
context.l10n.chat_sendMessageTo(
|
||||||
|
_resolveContact(context.read<MeshCoreConnector>()).name,
|
||||||
|
),
|
||||||
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
|
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -269,6 +377,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
|
|
||||||
// Auto-scroll to bottom if user is already at bottom
|
// Auto-scroll to bottom if user is already at bottom
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
_scrollController.scrollToBottomIfAtBottom();
|
_scrollController.scrollToBottomIfAtBottom();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -293,10 +402,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
final messageIndex = index;
|
final messageIndex = index;
|
||||||
Contact contact = widget.contact;
|
Contact contact = _resolveContact(connector);
|
||||||
final message = reversedMessages[messageIndex];
|
final message = reversedMessages[messageIndex];
|
||||||
String fourByteHex = '';
|
String fourByteHex = '';
|
||||||
if (widget.contact.type == advTypeRoom) {
|
if (contact.type == advTypeRoom) {
|
||||||
contact = _resolveContactFrom4Bytes(
|
contact = _resolveContactFrom4Bytes(
|
||||||
connector,
|
connector,
|
||||||
message.fourByteRoomContactKey.isEmpty
|
message.fourByteRoomContactKey.isEmpty
|
||||||
@@ -314,15 +423,18 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
final textScale = context.select<ChatTextScaleService, double>(
|
final textScale = context.select<ChatTextScaleService, double>(
|
||||||
(service) => service.scale,
|
(service) => service.scale,
|
||||||
);
|
);
|
||||||
|
final resolvedContact = _resolveContact(connector);
|
||||||
return _MessageBubble(
|
return _MessageBubble(
|
||||||
message: message,
|
message: message,
|
||||||
senderName: widget.contact.type == advTypeRoom
|
senderName: resolvedContact.type == advTypeRoom
|
||||||
? "${contact.name} [$fourByteHex]"
|
? "${contact.name} [$fourByteHex]"
|
||||||
: contact.name,
|
: contact.name,
|
||||||
isRoomServer: widget.contact.type == advTypeRoom,
|
isRoomServer: resolvedContact.type == advTypeRoom,
|
||||||
textScale: textScale,
|
textScale: textScale,
|
||||||
onTap: () => _openMessagePath(message, contact),
|
onTap: () => _openMessagePath(message, contact),
|
||||||
onLongPress: () => _showMessageActions(message, contact),
|
onLongPress: () => _showMessageActions(message, contact),
|
||||||
|
onRetryReaction: (msg, emoji) =>
|
||||||
|
_sendReaction(msg, contact, emoji),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -457,7 +569,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
connector.sendMessage(widget.contact, text);
|
connector.sendMessage(_resolveContact(connector), text);
|
||||||
_textController.clear();
|
_textController.clear();
|
||||||
_textFieldFocusNode.requestFocus();
|
_textFieldFocusNode.requestFocus();
|
||||||
}
|
}
|
||||||
@@ -654,7 +766,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
|
|
||||||
// Set the path override to persist user's choice
|
// Set the path override to persist user's choice
|
||||||
await connector.setPathOverride(
|
await connector.setPathOverride(
|
||||||
widget.contact,
|
_resolveContact(connector),
|
||||||
pathLen: pathLength,
|
pathLen: pathLength,
|
||||||
pathBytes: pathBytes,
|
pathBytes: pathBytes,
|
||||||
);
|
);
|
||||||
@@ -663,7 +775,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
await _notifyPathSet(
|
await _notifyPathSet(
|
||||||
connector,
|
connector,
|
||||||
widget.contact,
|
_resolveContact(connector),
|
||||||
pathBytes,
|
pathBytes,
|
||||||
path.hopCount,
|
path.hopCount,
|
||||||
);
|
);
|
||||||
@@ -722,7 +834,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
style: const TextStyle(fontSize: 11),
|
style: const TextStyle(fontSize: 11),
|
||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await connector.clearContactPath(widget.contact);
|
await connector.clearContactPath(
|
||||||
|
_resolveContact(connector),
|
||||||
|
);
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
@@ -750,7 +864,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await connector.setPathOverride(
|
await connector.setPathOverride(
|
||||||
widget.contact,
|
_resolveContact(connector),
|
||||||
pathLen: -1,
|
pathLen: -1,
|
||||||
);
|
);
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
@@ -779,7 +893,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatRelativeTime(DateTime time) {
|
String _formatRelativeTime(DateTime? time) {
|
||||||
|
if (time == null) return '—';
|
||||||
final diff = DateTime.now().difference(time);
|
final diff = DateTime.now().difference(time);
|
||||||
if (diff.inSeconds < 60) return context.l10n.time_justNow;
|
if (diff.inSeconds < 60) return context.l10n.time_justNow;
|
||||||
if (diff.inMinutes < 60) {
|
if (diff.inMinutes < 60) {
|
||||||
@@ -800,15 +915,31 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final formattedPath = pathBytes
|
final connector = context.read<MeshCoreConnector>();
|
||||||
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
final allContacts = connector.allContacts;
|
||||||
.join(',');
|
|
||||||
|
final formattedPath = PathHelper.formatPathHex(pathBytes);
|
||||||
|
final resolvedNames = PathHelper.resolvePathNames(pathBytes, allContacts);
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: Text(context.l10n.chat_fullPath),
|
title: Text(context.l10n.chat_fullPath),
|
||||||
content: SelectableText(formattedPath),
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SelectableText(formattedPath),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SelectableText(
|
||||||
|
resolvedNames,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.push(
|
onPressed: () => Navigator.push(
|
||||||
@@ -817,7 +948,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
builder: (context) => PathTraceMapScreen(
|
builder: (context) => PathTraceMapScreen(
|
||||||
title: context.l10n.contacts_repeaterPathTrace,
|
title: context.l10n.contacts_repeaterPathTrace,
|
||||||
path: Uint8List.fromList(pathBytes),
|
path: Uint8List.fromList(pathBytes),
|
||||||
flipPathRound: true,
|
flipPathAround: true,
|
||||||
|
targetContact: widget.contact,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -832,11 +964,22 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int _resolveContactIndex = -1;
|
||||||
|
|
||||||
Contact _resolveContact(MeshCoreConnector connector) {
|
Contact _resolveContact(MeshCoreConnector connector) {
|
||||||
return connector.contacts.firstWhere(
|
if (_resolveContactIndex >= 0 &&
|
||||||
|
_resolveContactIndex < connector.contacts.length &&
|
||||||
|
connector.contacts[_resolveContactIndex].publicKeyHex ==
|
||||||
|
widget.contact.publicKeyHex) {
|
||||||
|
return connector.contacts[_resolveContactIndex];
|
||||||
|
}
|
||||||
|
_resolveContactIndex = connector.contacts.indexWhere(
|
||||||
(c) => c.publicKeyHex == widget.contact.publicKeyHex,
|
(c) => c.publicKeyHex == widget.contact.publicKeyHex,
|
||||||
orElse: () => widget.contact,
|
|
||||||
);
|
);
|
||||||
|
if (_resolveContactIndex == -1) {
|
||||||
|
return widget.contact;
|
||||||
|
}
|
||||||
|
return connector.contacts[_resolveContactIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
Contact _resolveContactFrom4Bytes(
|
Contact _resolveContactFrom4Bytes(
|
||||||
@@ -889,19 +1032,11 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
|
|
||||||
void _showContactInfo(BuildContext context) {
|
void _showContactInfo(BuildContext context) {
|
||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
connector.ensureContactSmazSettingLoaded(widget.contact.publicKeyHex);
|
final contact = _resolveContact(connector);
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => Consumer<MeshCoreConnector>(
|
builder: (context) => AlertDialog(
|
||||||
builder: (context, connector, _) {
|
title: SelectableText(contact.name),
|
||||||
final contact = _resolveContact(connector);
|
|
||||||
final smazEnabled = connector.isContactSmazEnabled(
|
|
||||||
contact.publicKeyHex,
|
|
||||||
);
|
|
||||||
|
|
||||||
return AlertDialog(
|
|
||||||
title: Text(contact.name),
|
|
||||||
content: SingleChildScrollView(
|
content: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -909,16 +1044,54 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
children: [
|
children: [
|
||||||
_buildInfoRow(context.l10n.chat_type, contact.typeLabel),
|
_buildInfoRow(context.l10n.chat_type, contact.typeLabel),
|
||||||
_buildInfoRow(context.l10n.chat_path, contact.pathLabel),
|
_buildInfoRow(context.l10n.chat_path, contact.pathLabel),
|
||||||
|
_buildInfoRow(
|
||||||
|
context.l10n.contact_lastSeen,
|
||||||
|
_formatContactLastMessage(contact.lastMessageAt),
|
||||||
|
),
|
||||||
if (contact.hasLocation)
|
if (contact.hasLocation)
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
context.l10n.chat_location,
|
context.l10n.chat_location,
|
||||||
'${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}',
|
'${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}',
|
||||||
),
|
),
|
||||||
_buildInfoRow(
|
_buildInfoRow(context.l10n.chat_publicKey, contact.publicKeyHex),
|
||||||
context.l10n.chat_publicKey,
|
],
|
||||||
'${contact.publicKeyHex.substring(0, 16)}...',
|
|
||||||
),
|
),
|
||||||
const Divider(),
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(context.l10n.common_close),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showContactSettings(BuildContext context) {
|
||||||
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
|
connector.ensureContactSmazSettingLoaded(widget.contact.publicKeyHex);
|
||||||
|
final contact = widget.contact;
|
||||||
|
bool smazEnabled = connector.isContactSmazEnabled(contact.publicKeyHex);
|
||||||
|
bool teleBaseEnabled = contact.teleBaseEnabled;
|
||||||
|
bool teleLocEnabled = contact.teleLocEnabled;
|
||||||
|
bool teleEnvEnabled = contact.teleEnvEnabled;
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => StatefulBuilder(
|
||||||
|
builder: (context, setDialogState) => AlertDialog(
|
||||||
|
title: Text(context.l10n.contact_settings),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (contact.hasLocation) ...[
|
||||||
|
_buildInfoRow(
|
||||||
|
context.l10n.chat_location,
|
||||||
|
'${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}',
|
||||||
|
),
|
||||||
|
const Divider(height: 8),
|
||||||
|
],
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
title: Text(context.l10n.channels_smazCompression),
|
title: Text(context.l10n.channels_smazCompression),
|
||||||
@@ -929,6 +1102,37 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
contact.publicKeyHex,
|
contact.publicKeyHex,
|
||||||
value,
|
value,
|
||||||
);
|
);
|
||||||
|
setDialogState(() => smazEnabled = value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Divider(height: 8),
|
||||||
|
SwitchListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: Text(context.l10n.contact_teleBase),
|
||||||
|
subtitle: Text(context.l10n.contact_teleBaseSubtitle),
|
||||||
|
value: teleBaseEnabled,
|
||||||
|
onChanged: (value) {
|
||||||
|
setDialogState(() => teleBaseEnabled = value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Divider(height: 8),
|
||||||
|
SwitchListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: Text(context.l10n.contact_teleLoc),
|
||||||
|
subtitle: Text(context.l10n.contact_teleLocSubtitle),
|
||||||
|
value: teleLocEnabled,
|
||||||
|
onChanged: (value) {
|
||||||
|
setDialogState(() => teleLocEnabled = value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Divider(height: 8),
|
||||||
|
SwitchListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: Text(context.l10n.contact_teleEnv),
|
||||||
|
subtitle: Text(context.l10n.contact_teleEnvSubtitle),
|
||||||
|
value: teleEnvEnabled,
|
||||||
|
onChanged: (value) {
|
||||||
|
setDialogState(() => teleEnvEnabled = value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -936,12 +1140,19 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () {
|
||||||
|
connector.setContactFlags(
|
||||||
|
contact,
|
||||||
|
teleBase: teleBaseEnabled,
|
||||||
|
teleLoc: teleLocEnabled,
|
||||||
|
teleEnv: teleEnvEnabled,
|
||||||
|
);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
child: Text(context.l10n.common_close),
|
child: Text(context.l10n.common_close),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -956,12 +1167,32 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
width: 80,
|
width: 80,
|
||||||
child: Text(label, style: TextStyle(color: Colors.grey[600])),
|
child: Text(label, style: TextStyle(color: Colors.grey[600])),
|
||||||
),
|
),
|
||||||
Expanded(child: Text(value)),
|
Expanded(child: SelectableText(value)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _formatContactLastMessage(DateTime timestamp) {
|
||||||
|
final diff = DateTime.now().difference(timestamp);
|
||||||
|
if (diff.isNegative || diff.inMinutes < 5) {
|
||||||
|
return context.l10n.contacts_lastSeenNow;
|
||||||
|
}
|
||||||
|
if (diff.inMinutes < 60) {
|
||||||
|
return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes);
|
||||||
|
}
|
||||||
|
if (diff.inHours < 24) {
|
||||||
|
final hours = diff.inHours;
|
||||||
|
return hours == 1
|
||||||
|
? context.l10n.contacts_lastSeenHourAgo
|
||||||
|
: context.l10n.contacts_lastSeenHoursAgo(hours);
|
||||||
|
}
|
||||||
|
final days = diff.inDays;
|
||||||
|
return days == 1
|
||||||
|
? context.l10n.contacts_lastSeenDayAgo
|
||||||
|
: context.l10n.contacts_lastSeenDaysAgo(days);
|
||||||
|
}
|
||||||
|
|
||||||
void _openChat(BuildContext context, Contact contact) {
|
void _openChat(BuildContext context, Contact contact) {
|
||||||
// Check if this is a repeater
|
// Check if this is a repeater
|
||||||
context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex);
|
context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex);
|
||||||
@@ -985,7 +1216,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
final currentPathLabel = _currentPathLabel(currentContact);
|
final currentPathLabel = _currentPathLabel(currentContact);
|
||||||
|
|
||||||
// Filter out the current contact from available contacts
|
// Filter out the current contact from available contacts
|
||||||
final availableContacts = connector.contacts
|
final availableContacts = connector.allContacts
|
||||||
.where((c) => c != widget.contact)
|
.where((c) => c != widget.contact)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
@@ -1004,11 +1235,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
appLogger.info(
|
return; // Cancelled — keep existing path
|
||||||
'PathSelectionDialog was cancelled or returned null',
|
|
||||||
tag: 'ChatScreen',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
@@ -1024,14 +1251,19 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
tag: 'ChatScreen',
|
tag: 'ChatScreen',
|
||||||
);
|
);
|
||||||
await connector.setPathOverride(
|
await connector.setPathOverride(
|
||||||
widget.contact,
|
_resolveContact(connector),
|
||||||
pathLen: result.length,
|
pathLen: result.length,
|
||||||
pathBytes: result,
|
pathBytes: result,
|
||||||
);
|
);
|
||||||
appLogger.info('setPathOverride completed', tag: 'ChatScreen');
|
appLogger.info('setPathOverride completed', tag: 'ChatScreen');
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
await _notifyPathSet(connector, widget.contact, result, result.length);
|
await _notifyPathSet(
|
||||||
|
connector,
|
||||||
|
_resolveContact(connector),
|
||||||
|
result,
|
||||||
|
result.length,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _openMessagePath(Message message, Contact contact) {
|
void _openMessagePath(Message message, Contact contact) {
|
||||||
@@ -1043,10 +1275,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
final String senderName;
|
final String senderName;
|
||||||
if (message.isOutgoing) {
|
if (message.isOutgoing) {
|
||||||
senderName = connector.selfName ?? context.l10n.chat_me;
|
senderName = connector.selfName ?? context.l10n.chat_me;
|
||||||
} else if (widget.contact.type == advTypeRoom) {
|
} else if (_resolveContact(connector).type == advTypeRoom) {
|
||||||
senderName = "${contact.name} [$fourByteHex]";
|
senderName = "${contact.name} [$fourByteHex]";
|
||||||
} else {
|
} else {
|
||||||
senderName = widget.contact.name;
|
senderName = _resolveContact(connector).name;
|
||||||
}
|
}
|
||||||
final pathMessage = ChannelMessage(
|
final pathMessage = ChannelMessage(
|
||||||
senderKey: null,
|
senderKey: null,
|
||||||
@@ -1084,6 +1316,15 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
_showEmojiPicker(message, contact);
|
_showEmojiPicker(message, contact);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (PlatformInfo.isDesktop)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.route),
|
||||||
|
title: Text(context.l10n.chat_path),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(sheetContext);
|
||||||
|
_openMessagePath(message, contact);
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.copy),
|
leading: const Icon(Icons.copy),
|
||||||
title: Text(context.l10n.common_copy),
|
title: Text(context.l10n.common_copy),
|
||||||
@@ -1109,7 +1350,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
_retryMessage(message);
|
_retryMessage(message);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (widget.contact.type == advTypeRoom)
|
if (_resolveContact(context.read<MeshCoreConnector>()).type ==
|
||||||
|
advTypeRoom)
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.chat),
|
leading: const Icon(Icons.chat),
|
||||||
title: Text(context.l10n.contacts_openChat),
|
title: Text(context.l10n.contacts_openChat),
|
||||||
@@ -1147,7 +1389,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
void _retryMessage(Message message) {
|
void _retryMessage(Message message) {
|
||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
// Retry using the contact's current path override setting
|
// Retry using the contact's current path override setting
|
||||||
connector.sendMessage(widget.contact, message.text);
|
connector.sendMessage(_resolveContact(connector), message.text);
|
||||||
ScaffoldMessenger.of(
|
ScaffoldMessenger.of(
|
||||||
context,
|
context,
|
||||||
).showSnackBar(SnackBar(content: Text(context.l10n.chat_retryingMessage)));
|
).showSnackBar(SnackBar(content: Text(context.l10n.chat_retryingMessage)));
|
||||||
@@ -1173,7 +1415,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
|
|
||||||
// For room servers, include sender name (like channels) since multiple users
|
// For room servers, include sender name (like channels) since multiple users
|
||||||
// For 1:1 chats, sender is implicit (null)
|
// For 1:1 chats, sender is implicit (null)
|
||||||
final senderName = widget.contact.type == advTypeRoom
|
final liveContact = _resolveContact(connector);
|
||||||
|
final senderName = liveContact.type == advTypeRoom
|
||||||
? senderContact.name
|
? senderContact.name
|
||||||
: null;
|
: null;
|
||||||
final hash = ReactionHelper.computeReactionHash(
|
final hash = ReactionHelper.computeReactionHash(
|
||||||
@@ -1182,7 +1425,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
message.text,
|
message.text,
|
||||||
);
|
);
|
||||||
final reactionText = 'r:$hash:$emojiIndex';
|
final reactionText = 'r:$hash:$emojiIndex';
|
||||||
connector.sendMessage(widget.contact, reactionText);
|
connector.sendMessage(_resolveContact(connector), reactionText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1192,6 +1435,7 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
final bool isRoomServer;
|
final bool isRoomServer;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
final VoidCallback? onLongPress;
|
final VoidCallback? onLongPress;
|
||||||
|
final void Function(Message message, String emoji)? onRetryReaction;
|
||||||
final double textScale;
|
final double textScale;
|
||||||
|
|
||||||
const _MessageBubble({
|
const _MessageBubble({
|
||||||
@@ -1201,6 +1445,7 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
required this.textScale,
|
required this.textScale,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.onLongPress,
|
this.onLongPress,
|
||||||
|
this.onRetryReaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -1234,8 +1479,11 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
: CrossAxisAlignment.start,
|
: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: onTap,
|
onTap: PlatformInfo.isDesktop ? null : onTap,
|
||||||
onLongPress: onLongPress,
|
onLongPress: onLongPress,
|
||||||
|
onSecondaryTapUp: PlatformInfo.isDesktop
|
||||||
|
? (_) => onLongPress?.call()
|
||||||
|
: null,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: isOutgoing
|
mainAxisAlignment: isOutgoing
|
||||||
? MainAxisAlignment.end
|
? MainAxisAlignment.end
|
||||||
@@ -1352,7 +1600,8 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Linkify(
|
child: LinkHandler.buildLinkifyText(
|
||||||
|
context: context,
|
||||||
text: messageText,
|
text: messageText,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: textColor,
|
color: textColor,
|
||||||
@@ -1363,15 +1612,6 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
decoration: TextDecoration.underline,
|
decoration: TextDecoration.underline,
|
||||||
fontSize: bodyFontSize * textScale,
|
fontSize: bodyFontSize * textScale,
|
||||||
),
|
),
|
||||||
options: const LinkifyOptions(
|
|
||||||
humanize: false,
|
|
||||||
defaultToHttps: false,
|
|
||||||
),
|
|
||||||
linkifiers: const [UrlLinkifier()],
|
|
||||||
onOpen: (link) => LinkHandler.handleLinkTap(
|
|
||||||
context,
|
|
||||||
link.url,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!enableTracing && isOutgoing) ...[
|
if (!enableTracing && isOutgoing) ...[
|
||||||
@@ -1561,14 +1801,28 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
children: message.reactions.entries.map((entry) {
|
children: message.reactions.entries.map((entry) {
|
||||||
final emoji = entry.key;
|
final emoji = entry.key;
|
||||||
final count = entry.value;
|
final count = entry.value;
|
||||||
|
final status = message.reactionStatuses[emoji];
|
||||||
|
final isPending =
|
||||||
|
status == MessageStatus.pending || status == MessageStatus.sent;
|
||||||
|
final isFailed = status == MessageStatus.failed;
|
||||||
|
|
||||||
return Container(
|
return GestureDetector(
|
||||||
|
onTap: isFailed && onRetryReaction != null
|
||||||
|
? () => onRetryReaction!(message, emoji)
|
||||||
|
: null,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: isPending ? 0.5 : 1.0,
|
||||||
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: colorScheme.secondaryContainer,
|
color: isFailed
|
||||||
|
? colorScheme.errorContainer
|
||||||
|
: colorScheme.secondaryContainer,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: colorScheme.outline.withValues(alpha: 0.3),
|
color: isFailed
|
||||||
|
? colorScheme.error
|
||||||
|
: colorScheme.outline.withValues(alpha: 0.3),
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1587,7 +1841,24 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
if (isPending) ...[
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
SizedBox(
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 1.5,
|
||||||
|
color: colorScheme.onSecondaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
|
if (isFailed) ...[
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
Icon(Icons.replay, size: 10, color: colorScheme.error),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,6 +51,9 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
|
|||||||
_isProcessing = true;
|
_isProcessing = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final connector = context.read<MeshCoreConnector>();
|
||||||
|
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Parse the community data
|
// Parse the community data
|
||||||
final community = Community.fromQrData(const Uuid().v4(), data);
|
final community = Community.fromQrData(const Uuid().v4(), data);
|
||||||
@@ -209,6 +212,8 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
|
|||||||
bool addPublicChannel,
|
bool addPublicChannel,
|
||||||
) async {
|
) async {
|
||||||
// Save community to local storage
|
// Save community to local storage
|
||||||
|
final connector = context.read<MeshCoreConnector>();
|
||||||
|
_communityStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||||
await _communityStore.addCommunity(community);
|
await _communityStore.addCommunity(community);
|
||||||
|
|
||||||
// Optionally add the community public channel to the device
|
// Optionally add the community public channel to the device
|
||||||
|
|||||||
+459
-231
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,429 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../connector/meshcore_connector.dart';
|
||||||
|
import '../connector/meshcore_protocol.dart';
|
||||||
|
import '../l10n/l10n.dart';
|
||||||
|
import '../models/contact.dart';
|
||||||
|
import '../utils/contact_search.dart';
|
||||||
|
import '../utils/platform_info.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];
|
||||||
|
final tile = 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),
|
||||||
|
);
|
||||||
|
if (PlatformInfo.isDesktop) {
|
||||||
|
return GestureDetector(
|
||||||
|
onSecondaryTapUp: (_) =>
|
||||||
|
_showContactContextMenu(contact, connector),
|
||||||
|
child: tile,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return tile;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showContactContextMenu(
|
||||||
|
Contact contact,
|
||||||
|
MeshCoreConnector connector,
|
||||||
|
) async {
|
||||||
|
final action = await showModalBottomSheet<String>(
|
||||||
|
context: context,
|
||||||
|
showDragHandle: true,
|
||||||
|
builder: (sheetContext) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.add_reaction_sharp),
|
||||||
|
title: Text(l10n.discoveredContacts_addContact),
|
||||||
|
onTap: () => Navigator.of(sheetContext).pop('import_contact'),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.copy),
|
||||||
|
title: Text(l10n.discoveredContacts_copyContact),
|
||||||
|
onTap: () => Navigator.of(sheetContext).pop('copy_contact'),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.delete),
|
||||||
|
title: Text(l10n.discoveredContacts_deleteContact),
|
||||||
|
onTap: () => Navigator.of(sheetContext).pop('delete_contact'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mounted || action == null) return;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'import_contact':
|
||||||
|
connector.importDiscoveredContact(contact);
|
||||||
|
break;
|
||||||
|
case 'copy_contact':
|
||||||
|
if (contact.rawPacket == null) return;
|
||||||
|
final hexString = pubKeyToHex(contact.rawPacket!);
|
||||||
|
Clipboard.setData(ClipboardData(text: "meshcore://$hexString"));
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'delete_contact':
|
||||||
|
connector.removeDiscoveredContact(contact);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _deleteContacts(BuildContext context, MeshCoreConnector connector) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text(l10n.common_deleteAll),
|
||||||
|
content: Text(l10n.discoveredContacts_deleteContactAllContent),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(l10n.common_cancel),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.pop(context);
|
||||||
|
connector.removeAllDiscoveredContacts();
|
||||||
|
},
|
||||||
|
child: Text(l10n.common_deleteAll),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFilters(
|
||||||
|
List<Contact> filteredAndSorted,
|
||||||
|
MeshCoreConnector connector,
|
||||||
|
) {
|
||||||
|
String hintText = "";
|
||||||
|
switch (typeFilter) {
|
||||||
|
case ContactTypeFilter.all:
|
||||||
|
hintText = context.l10n.contacts_searchContacts(
|
||||||
|
filteredAndSorted.length,
|
||||||
|
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ContactTypeFilter.users:
|
||||||
|
hintText = context.l10n.contacts_searchUsers(
|
||||||
|
filteredAndSorted.length,
|
||||||
|
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ContactTypeFilter.repeaters:
|
||||||
|
hintText = context.l10n.contacts_searchRepeaters(
|
||||||
|
filteredAndSorted.length,
|
||||||
|
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ContactTypeFilter.rooms:
|
||||||
|
hintText = context.l10n.contacts_searchRoomServers(
|
||||||
|
filteredAndSorted.length,
|
||||||
|
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ContactTypeFilter.favorites:
|
||||||
|
hintText = context.l10n.contacts_searchFavorites(
|
||||||
|
filteredAndSorted.length,
|
||||||
|
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: TextField(
|
||||||
|
controller: _searchController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: hintText,
|
||||||
|
prefixIcon: const Icon(Icons.search),
|
||||||
|
suffixIcon: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (searchQuery.isNotEmpty)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
onPressed: () {
|
||||||
|
_searchController.clear();
|
||||||
|
setState(() {
|
||||||
|
searchQuery = '';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_buildFilterButton(context, connector),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
_searchDebounce?.cancel();
|
||||||
|
_searchDebounce = Timer(const Duration(milliseconds: 300), () {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
searchQuery = value.toLowerCase();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFilterButton(BuildContext context, MeshCoreConnector connector) {
|
||||||
|
return DiscoveryContactsFilterMenu(
|
||||||
|
sortOption: sortOption,
|
||||||
|
typeFilter: typeFilter,
|
||||||
|
onSortChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
sortOption = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onTypeFilterChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
typeFilter = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Contact> _filterAndSortContacts(
|
||||||
|
List<Contact> contacts,
|
||||||
|
MeshCoreConnector connector,
|
||||||
|
) {
|
||||||
|
var filtered = contacts.where((contact) {
|
||||||
|
if (searchQuery.isEmpty) return true;
|
||||||
|
return matchesDiscoveryContactQuery(contact, searchQuery);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
filtered = filtered.where((contact) {
|
||||||
|
return !connector.knownContactKeys.contains(contact.publicKeyHex);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
// Filter out own node from the list
|
||||||
|
if (connector.selfPublicKey != null) {
|
||||||
|
final selfPubKeyHex = pubKeyToHex(connector.selfPublicKey!);
|
||||||
|
filtered = filtered.where((contact) {
|
||||||
|
return contact.publicKeyHex != selfPubKeyHex;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeFilter != ContactTypeFilter.all) {
|
||||||
|
filtered = filtered.where(_matchesTypeFilter).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (sortOption) {
|
||||||
|
case ContactSortOption.lastSeen:
|
||||||
|
filtered.sort((a, b) => b.lastSeen.compareTo(a.lastSeen));
|
||||||
|
break;
|
||||||
|
case ContactSortOption.name:
|
||||||
|
filtered.sort(
|
||||||
|
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _matchesTypeFilter(Contact contact) {
|
||||||
|
switch (typeFilter) {
|
||||||
|
case ContactTypeFilter.all:
|
||||||
|
return true;
|
||||||
|
case ContactTypeFilter.users:
|
||||||
|
return contact.type == advTypeChat;
|
||||||
|
case ContactTypeFilter.repeaters:
|
||||||
|
return contact.type == advTypeRepeater;
|
||||||
|
case ContactTypeFilter.rooms:
|
||||||
|
return contact.type == advTypeRoom;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData _getTypeIcon(int type) {
|
||||||
|
switch (type) {
|
||||||
|
case advTypeChat:
|
||||||
|
return Icons.chat;
|
||||||
|
case advTypeRepeater:
|
||||||
|
return Icons.cell_tower;
|
||||||
|
case advTypeRoom:
|
||||||
|
return Icons.group;
|
||||||
|
case advTypeSensor:
|
||||||
|
return Icons.sensors;
|
||||||
|
default:
|
||||||
|
return Icons.device_unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getTypeColor(int type) {
|
||||||
|
switch (type) {
|
||||||
|
case advTypeChat:
|
||||||
|
return Colors.blue;
|
||||||
|
case advTypeRepeater:
|
||||||
|
return Colors.orange;
|
||||||
|
case advTypeRoom:
|
||||||
|
return Colors.purple;
|
||||||
|
case advTypeSensor:
|
||||||
|
return Colors.green;
|
||||||
|
default:
|
||||||
|
return Colors.grey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatLastSeen(BuildContext context, DateTime lastSeen) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final diff = now.difference(lastSeen);
|
||||||
|
|
||||||
|
if (diff.isNegative || diff.inMinutes < 5) {
|
||||||
|
return context.l10n.contacts_lastSeenNow;
|
||||||
|
}
|
||||||
|
if (diff.inMinutes < 60) {
|
||||||
|
return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes);
|
||||||
|
}
|
||||||
|
if (diff.inHours < 24) {
|
||||||
|
final hours = diff.inHours;
|
||||||
|
return hours == 1
|
||||||
|
? context.l10n.contacts_lastSeenHourAgo
|
||||||
|
: context.l10n.contacts_lastSeenHoursAgo(hours);
|
||||||
|
}
|
||||||
|
final days = diff.inDays;
|
||||||
|
return days == 1
|
||||||
|
? context.l10n.contacts_lastSeenDayAgo
|
||||||
|
: context.l10n.contacts_lastSeenDaysAgo(days);
|
||||||
|
}
|
||||||
|
}
|
||||||
+581
-54
@@ -1,6 +1,8 @@
|
|||||||
|
import 'dart:collection';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
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';
|
||||||
@@ -15,6 +17,7 @@ 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';
|
||||||
@@ -49,7 +52,8 @@ class MapScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MapScreenState extends State<MapScreen> {
|
class _MapScreenState extends State<MapScreen> {
|
||||||
static const double _labelZoomThreshold = 8.5;
|
// Zoom level at which node labels start to appear
|
||||||
|
static const double _labelZoomThreshold = 14.0;
|
||||||
|
|
||||||
final MapController _mapController = MapController();
|
final MapController _mapController = MapController();
|
||||||
final MapMarkerService _markerService = MapMarkerService();
|
final MapMarkerService _markerService = MapMarkerService();
|
||||||
@@ -64,6 +68,8 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
final List<Polyline> _polylines = [];
|
final List<Polyline> _polylines = [];
|
||||||
bool _legendExpanded = false;
|
bool _legendExpanded = false;
|
||||||
bool _showNodeLabels = true;
|
bool _showNodeLabels = true;
|
||||||
|
List<_GuessedLocation> _cachedGuessedLocations = [];
|
||||||
|
String _guessedLocationsCacheKey = '';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -88,6 +94,15 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _checkLocationPlausibility(double lat, double lon) {
|
||||||
|
const double epsilon = 1e-6;
|
||||||
|
return (lat.abs() > epsilon || lon.abs() > epsilon) &&
|
||||||
|
lat >= -90.0 &&
|
||||||
|
lat <= 90.0 &&
|
||||||
|
lon >= -180.0 &&
|
||||||
|
lon <= 180.0;
|
||||||
|
}
|
||||||
|
|
||||||
double _standardDeviation(List<double> values) {
|
double _standardDeviation(List<double> values) {
|
||||||
if (values.length <= 1) {
|
if (values.length <= 1) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
@@ -119,11 +134,16 @@ 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 allContacts = connector.allContacts;
|
||||||
|
|
||||||
|
final contacts = settings.mapShowDiscoveryContacts
|
||||||
|
? allContacts
|
||||||
|
: allContacts.where((c) => c.isActive).toList();
|
||||||
|
|
||||||
final highlightPosition = widget.highlightPosition;
|
final highlightPosition = widget.highlightPosition;
|
||||||
final sharedMarkers = settings.mapShowMarkers
|
final sharedMarkers = settings.mapShowMarkers
|
||||||
? _collectSharedMarkers(connector)
|
? _collectSharedMarkers(connector)
|
||||||
@@ -156,10 +176,44 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
: filteredByTime;
|
: filteredByTime;
|
||||||
|
|
||||||
// Filter by location
|
// Filter by location
|
||||||
final contactsWithLocation = filteredByKeyPrefix
|
final contactsWithLocation = filteredByKeyPrefix.where((c) {
|
||||||
|
return c.hasLocation;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
// All contacts with a known location — used as anchors regardless of
|
||||||
|
// time/key-prefix filters so that repeaters are always available.
|
||||||
|
final allContactsWithLocation = allContacts
|
||||||
.where((c) => c.hasLocation)
|
.where((c) => c.hasLocation)
|
||||||
.toList();
|
.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
|
||||||
@@ -276,7 +330,9 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
if (!_isBuildingPathTrace)
|
if (!_isBuildingPathTrace)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.radar),
|
icon: const Icon(Icons.radar),
|
||||||
onPressed: () => _startPath(),
|
onPressed: () => _startPath(
|
||||||
|
LatLng(connector.selfLatitude!, connector.selfLongitude!),
|
||||||
|
),
|
||||||
tooltip: context.l10n.contacts_pathTrace,
|
tooltip: context.l10n.contacts_pathTrace,
|
||||||
),
|
),
|
||||||
if (!_isBuildingPathTrace)
|
if (!_isBuildingPathTrace)
|
||||||
@@ -424,12 +480,19 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
point: highlightPosition,
|
point: highlightPosition,
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
|
child: IgnorePointer(
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.location_on_outlined,
|
Icons.location_on_outlined,
|
||||||
color: Colors.red[600],
|
color: Colors.red[600],
|
||||||
size: 34,
|
size: 34,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
if (!_isBuildingPathTrace)
|
||||||
|
..._buildGuessedMarker(
|
||||||
|
guessedLocations,
|
||||||
|
showLabels: _showNodeLabels,
|
||||||
|
),
|
||||||
..._buildMarkers(
|
..._buildMarkers(
|
||||||
contactsWithLocation,
|
contactsWithLocation,
|
||||||
settings,
|
settings,
|
||||||
@@ -445,6 +508,8 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
),
|
),
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
|
child: IgnorePointer(
|
||||||
|
ignoring: true,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -456,7 +521,9 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
),
|
),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.3),
|
color: Colors.black.withValues(
|
||||||
|
alpha: 0.3,
|
||||||
|
),
|
||||||
blurRadius: 4,
|
blurRadius: 4,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
@@ -470,6 +537,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
if (_showNodeLabels &&
|
if (_showNodeLabels &&
|
||||||
connector.selfLatitude != null &&
|
connector.selfLatitude != null &&
|
||||||
connector.selfLongitude != null)
|
connector.selfLongitude != null)
|
||||||
@@ -486,9 +554,11 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
),
|
),
|
||||||
if (!_isBuildingPathTrace)
|
if (!_isBuildingPathTrace)
|
||||||
_buildLegend(
|
_buildLegend(
|
||||||
|
contacts,
|
||||||
contactsWithLocation,
|
contactsWithLocation,
|
||||||
settings,
|
settings,
|
||||||
sharedMarkers.length,
|
sharedMarkers.length,
|
||||||
|
guessedLocations.length,
|
||||||
),
|
),
|
||||||
if (_isBuildingPathTrace) _buildPathTraceOverlay(),
|
if (_isBuildingPathTrace) _buildPathTraceOverlay(),
|
||||||
],
|
],
|
||||||
@@ -512,31 +582,326 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<_GuessedLocation> _computeGuessedLocations(
|
||||||
|
List<Contact> allContacts,
|
||||||
|
List<Contact> withLocation,
|
||||||
|
PathHistoryService pathHistory,
|
||||||
|
double? maxRangeKm,
|
||||||
|
) {
|
||||||
|
// Index known-location repeaters by their 1-byte hash.
|
||||||
|
// null value = two repeaters share the same hash byte (ambiguous collision).
|
||||||
|
final repeaterByHash = <int, Contact?>{};
|
||||||
|
|
||||||
|
for (final c in withLocation) {
|
||||||
|
if (c.type == advTypeRepeater) {
|
||||||
|
if (repeaterByHash.containsKey(c.publicKey[0])) {
|
||||||
|
repeaterByHash[c.publicKey[0]] =
|
||||||
|
null; // collision: can't disambiguate
|
||||||
|
} else {
|
||||||
|
repeaterByHash[c.publicKey[0]] = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = <_GuessedLocation>[];
|
||||||
|
|
||||||
|
for (final contact in allContacts) {
|
||||||
|
if (contact.hasLocation) continue;
|
||||||
|
if (contact.lastSeen.isBefore(
|
||||||
|
DateTime.now().subtract(const Duration(hours: 24)),
|
||||||
|
)) {
|
||||||
|
continue; // skip stale contacts
|
||||||
|
}
|
||||||
|
|
||||||
|
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!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
// Spread single-anchor guesses around the anchor so they remain visible.
|
||||||
|
position = _offsetGuessedPosition(
|
||||||
|
anchors[0],
|
||||||
|
contact,
|
||||||
|
radiusMeters: 330,
|
||||||
|
);
|
||||||
|
if (!_checkLocationPlausibility(
|
||||||
|
position.latitude,
|
||||||
|
position.longitude,
|
||||||
|
)) {
|
||||||
|
continue; // discard implausible guesses near (0, 0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
double lat = 0, lon = 0, weight = 1.0;
|
||||||
|
int counted = 0;
|
||||||
|
for (final a in anchors) {
|
||||||
|
if (counted == 0) {
|
||||||
|
lat = a.latitude;
|
||||||
|
lon = a.longitude;
|
||||||
|
} else {
|
||||||
|
lat += a.latitude * weight;
|
||||||
|
lon += a.longitude * weight;
|
||||||
|
}
|
||||||
|
// weight subsequent anchors less to create a bias towards the first (if more than 2)
|
||||||
|
weight = weight / 2;
|
||||||
|
counted++;
|
||||||
|
}
|
||||||
|
position = _offsetGuessedPosition(
|
||||||
|
LatLng(lat / anchors.length, lon / anchors.length),
|
||||||
|
contact,
|
||||||
|
radiusMeters: anchors.length >= 3 ? 80 : 120,
|
||||||
|
);
|
||||||
|
if (!_checkLocationPlausibility(
|
||||||
|
position.latitude,
|
||||||
|
position.longitude,
|
||||||
|
)) {
|
||||||
|
continue; // discard implausible guesses near (0, 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.add(
|
||||||
|
_GuessedLocation(
|
||||||
|
contact: contact,
|
||||||
|
position: position,
|
||||||
|
highConfidence: anchors.length >= 2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
LatLng _offsetGuessedPosition(
|
||||||
|
LatLng anchor,
|
||||||
|
Contact contact, {
|
||||||
|
required double radiusMeters,
|
||||||
|
}) {
|
||||||
|
final seed = _guessSeed(contact.publicKey);
|
||||||
|
final angle = ((seed & 0xFFFF) / 0x10000) * 2 * pi;
|
||||||
|
final latOffsetDeg = (radiusMeters / 111320.0) * cos(angle);
|
||||||
|
final lonScale = max(cos(anchor.latitude * pi / 180.0).abs(), 0.2);
|
||||||
|
final lonOffsetDeg = (radiusMeters / (111320.0 * lonScale)) * sin(angle);
|
||||||
|
return LatLng(
|
||||||
|
anchor.latitude + latOffsetDeg,
|
||||||
|
anchor.longitude + lonOffsetDeg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int _guessSeed(Uint8List publicKey) {
|
||||||
|
var seed = 0x811C9DC5;
|
||||||
|
for (final byte in publicKey) {
|
||||||
|
seed ^= byte;
|
||||||
|
seed = (seed * 0x01000193) & 0x7FFFFFFF;
|
||||||
|
}
|
||||||
|
return seed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Estimates the free-space maximum LoRa range in km from the connected
|
||||||
|
/// device's current radio parameters. Returns null if parameters are unknown.
|
||||||
|
double? _estimateLoRaRangeKm(MeshCoreConnector connector) {
|
||||||
|
final freqHz = connector.currentFreqHz;
|
||||||
|
final bwHz = connector.currentBwHz;
|
||||||
|
final sf = connector.currentSf;
|
||||||
|
final txPower = connector.currentTxPower;
|
||||||
|
if (freqHz == null || bwHz == null || sf == null || txPower == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// LoRa receiver sensitivity = thermal noise + NF + required demod SNR
|
||||||
|
const noiseFigureDb = 6.0;
|
||||||
|
final thermalNoiseDbm = -174.0 + 10 * log(bwHz.toDouble()) / ln10;
|
||||||
|
final sensitivityDbm =
|
||||||
|
thermalNoiseDbm + noiseFigureDb + _sfToRequiredSnrDb(sf);
|
||||||
|
// FSPL at max range equals link budget:
|
||||||
|
// FSPL = 20*log10(d_m) + 20*log10(f_hz) - 147.55
|
||||||
|
final linkBudgetDb = txPower.toDouble() - sensitivityDbm;
|
||||||
|
final exponent =
|
||||||
|
(linkBudgetDb + 147.55 - 20 * log(freqHz.toDouble()) / ln10) / 20;
|
||||||
|
return pow(10, exponent) / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
double _sfToRequiredSnrDb(int sf) {
|
||||||
|
switch (sf) {
|
||||||
|
case 5:
|
||||||
|
return -2.5;
|
||||||
|
case 6:
|
||||||
|
return -5.0;
|
||||||
|
case 7:
|
||||||
|
return -7.5;
|
||||||
|
case 8:
|
||||||
|
return -10.0;
|
||||||
|
case 9:
|
||||||
|
return -12.5;
|
||||||
|
case 10:
|
||||||
|
return -15.0;
|
||||||
|
case 11:
|
||||||
|
return -17.5;
|
||||||
|
case 12:
|
||||||
|
return -20.0;
|
||||||
|
default:
|
||||||
|
return -10.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes anchors that have no neighbour within 2 * maxRangeKm.
|
||||||
|
/// A node cannot be simultaneously in radio range of two points farther apart
|
||||||
|
/// than twice the expected maximum range.
|
||||||
|
List<LatLng> _filterConsistentAnchors(
|
||||||
|
List<LatLng> anchors,
|
||||||
|
double maxRangeKm,
|
||||||
|
) {
|
||||||
|
const distance = Distance();
|
||||||
|
final maxDistM = maxRangeKm * 2000;
|
||||||
|
return anchors
|
||||||
|
.where((a) => anchors.any((b) => b != a && distance(a, b) <= maxDistM))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Marker> _buildGuessedMarker(
|
||||||
|
List<_GuessedLocation> guessed, {
|
||||||
|
required bool showLabels,
|
||||||
|
}) {
|
||||||
|
final markers = <Marker>[];
|
||||||
|
|
||||||
|
for (final guess in guessed) {
|
||||||
|
final color = _getNodeColor(guess.contact.type);
|
||||||
|
final marker = Marker(
|
||||||
|
point: guess.position,
|
||||||
|
width: 35,
|
||||||
|
height: 35,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => _showNodeInfo(
|
||||||
|
context,
|
||||||
|
guess.contact,
|
||||||
|
guessedPosition: guess.position,
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(
|
||||||
|
alpha: guess.highConfidence ? 0.55 : 0.30,
|
||||||
|
),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.3),
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.not_listed_location,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
markers.add(marker);
|
||||||
|
|
||||||
|
if (showLabels) {
|
||||||
|
markers.add(
|
||||||
|
_buildNodeLabelMarker(
|
||||||
|
point: guess.position,
|
||||||
|
label: guess.contact.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return markers;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Contact> _filterContactsBySettings(
|
||||||
|
List<Contact> contacts,
|
||||||
|
dynamic settings, {
|
||||||
|
bool noLocations = false,
|
||||||
|
}) {
|
||||||
|
List<Contact> filtered = [];
|
||||||
|
bool addContact = false;
|
||||||
|
for (final contact in contacts) {
|
||||||
|
addContact = false;
|
||||||
|
if (!contact.hasLocation && !noLocations) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply node type filters
|
||||||
|
if (contact.type == advTypeRepeater &&
|
||||||
|
(settings.mapShowRepeaters ||
|
||||||
|
_isBuildingPathTrace ||
|
||||||
|
settings.mapShowOverlaps)) {
|
||||||
|
addContact = true;
|
||||||
|
}
|
||||||
|
if (contact.type == advTypeChat &&
|
||||||
|
(settings.mapShowChatNodes || _isBuildingPathTrace)) {
|
||||||
|
addContact = true;
|
||||||
|
}
|
||||||
|
if (contact.type != advTypeChat &&
|
||||||
|
contact.type != advTypeRepeater &&
|
||||||
|
(settings.mapShowOtherNodes ||
|
||||||
|
_isBuildingPathTrace ||
|
||||||
|
settings.mapShowOverlaps)) {
|
||||||
|
addContact = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final hasOverlap = contacts
|
||||||
|
.where(
|
||||||
|
(c) =>
|
||||||
|
c.publicKeyHex != contact.publicKeyHex &&
|
||||||
|
c.publicKey.first == contact.publicKey.first &&
|
||||||
|
(c.type == advTypeRepeater || c.type == advTypeRoom) &&
|
||||||
|
(contact.type == advTypeRepeater ||
|
||||||
|
contact.type == advTypeRoom),
|
||||||
|
)
|
||||||
|
.firstOrNull;
|
||||||
|
|
||||||
|
if (hasOverlap == null &&
|
||||||
|
settings.mapShowOverlaps &&
|
||||||
|
!_isBuildingPathTrace) {
|
||||||
|
addContact = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addContact) {
|
||||||
|
filtered.add(contact);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
List<Marker> _buildMarkers(
|
List<Marker> _buildMarkers(
|
||||||
List<Contact> contacts,
|
List<Contact> contacts,
|
||||||
settings, {
|
settings, {
|
||||||
required bool showLabels,
|
required bool showLabels,
|
||||||
}) {
|
}) {
|
||||||
final markers = <Marker>[];
|
final markers = <Marker>[];
|
||||||
|
final filteredContacts = _filterContactsBySettings(contacts, settings);
|
||||||
for (final contact in contacts) {
|
for (final contact in filteredContacts) {
|
||||||
if (!contact.hasLocation) continue;
|
|
||||||
|
|
||||||
// Apply node type filters
|
|
||||||
if (contact.type == advTypeRepeater &&
|
|
||||||
(!settings.mapShowRepeaters && !_isBuildingPathTrace)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (contact.type == advTypeChat &&
|
|
||||||
!(settings.mapShowChatNodes && !_isBuildingPathTrace)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (contact.type != advTypeChat &&
|
|
||||||
contact.type != advTypeRepeater &&
|
|
||||||
(!settings.mapShowOtherNodes && !_isBuildingPathTrace)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
final marker = Marker(
|
final marker = Marker(
|
||||||
point: LatLng(contact.latitude!, contact.longitude!),
|
point: LatLng(contact.latitude!, contact.longitude!),
|
||||||
width: 35,
|
width: 35,
|
||||||
@@ -552,7 +917,9 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _getNodeColor(contact.type),
|
color: settings.mapShowOverlaps && !_isBuildingPathTrace
|
||||||
|
? Colors.red
|
||||||
|
: _getNodeColor(contact.type),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
border: Border.all(color: Colors.white, width: 2),
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
@@ -579,7 +946,9 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
markers.add(
|
markers.add(
|
||||||
_buildNodeLabelMarker(
|
_buildNodeLabelMarker(
|
||||||
point: LatLng(contact.latitude!, contact.longitude!),
|
point: LatLng(contact.latitude!, contact.longitude!),
|
||||||
label: contact.name,
|
label: settings.mapShowOverlaps && !_isBuildingPathTrace
|
||||||
|
? "${contact.publicKeyHex.substring(0, 2)}:${contact.name}"
|
||||||
|
: contact.name,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -654,24 +1023,25 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLegend(
|
Widget _buildLegend(
|
||||||
|
List<Contact> contacts,
|
||||||
List<Contact> contactsWithLocation,
|
List<Contact> contactsWithLocation,
|
||||||
settings,
|
settings,
|
||||||
int markerCount,
|
int markerCount,
|
||||||
|
int guessedCount,
|
||||||
) {
|
) {
|
||||||
int nodeCount = 0;
|
final filteredContacts = _filterContactsBySettings(
|
||||||
for (final contact in contactsWithLocation) {
|
contacts,
|
||||||
// Apply node type filters
|
settings,
|
||||||
if (contact.type == advTypeRepeater && !settings.mapShowRepeaters) {
|
noLocations: false,
|
||||||
continue;
|
);
|
||||||
}
|
final filteredContactsAll = _filterContactsBySettings(
|
||||||
if (contact.type == advTypeChat && !settings.mapShowChatNodes) continue;
|
contacts,
|
||||||
if (contact.type != advTypeChat &&
|
settings,
|
||||||
contact.type != advTypeRepeater &&
|
noLocations: true,
|
||||||
!settings.mapShowOtherNodes) {
|
);
|
||||||
continue;
|
|
||||||
}
|
final nodeCount = filteredContacts.length;
|
||||||
nodeCount++;
|
final nodeCountAll = filteredContactsAll.length;
|
||||||
}
|
|
||||||
|
|
||||||
return Positioned(
|
return Positioned(
|
||||||
top: 16,
|
top: 16,
|
||||||
@@ -696,12 +1066,65 @@ 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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.location_on,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
": $nodeCount",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.wrong_location,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
": ${nodeCountAll - nodeCount}",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.add_outlined,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
": $nodeCountAll",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
context.l10n.map_pinsCount(markerCount),
|
context.l10n.map_pinsCount(markerCount),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@@ -764,6 +1187,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,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -952,7 +1381,12 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showNodeInfo(BuildContext context, Contact contact) {
|
void _showNodeInfo(
|
||||||
|
BuildContext context,
|
||||||
|
Contact contact, {
|
||||||
|
LatLng? guessedPosition,
|
||||||
|
}) {
|
||||||
|
final connector = context.read<MeshCoreConnector>();
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (dialogContext) => AlertDialog(
|
builder: (dialogContext) => AlertDialog(
|
||||||
@@ -972,9 +1406,15 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
children: [
|
children: [
|
||||||
_buildInfoRow('Type', contact.typeLabel),
|
_buildInfoRow('Type', contact.typeLabel),
|
||||||
_buildInfoRow('Path', contact.pathLabel),
|
_buildInfoRow('Path', contact.pathLabel),
|
||||||
|
if (contact.hasLocation)
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
'Location',
|
'Location',
|
||||||
'${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}',
|
'${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,
|
||||||
@@ -992,6 +1432,9 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
advTypeChat) // Only show chat button for chat nodes
|
advTypeChat) // Only show chat button for chat nodes
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
if (!contact.isActive) {
|
||||||
|
connector.importDiscoveredContact(contact);
|
||||||
|
}
|
||||||
Navigator.pop(dialogContext);
|
Navigator.pop(dialogContext);
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
@@ -1005,6 +1448,9 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
if (contact.type == advTypeRepeater)
|
if (contact.type == advTypeRepeater)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
if (!contact.isActive) {
|
||||||
|
connector.importDiscoveredContact(contact);
|
||||||
|
}
|
||||||
Navigator.pop(dialogContext);
|
Navigator.pop(dialogContext);
|
||||||
_showRepeaterLogin(context, contact);
|
_showRepeaterLogin(context, contact);
|
||||||
},
|
},
|
||||||
@@ -1013,6 +1459,9 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
if (contact.type == advTypeRoom)
|
if (contact.type == advTypeRoom)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
if (!contact.isActive) {
|
||||||
|
connector.importDiscoveredContact(contact);
|
||||||
|
}
|
||||||
Navigator.pop(dialogContext);
|
Navigator.pop(dialogContext);
|
||||||
_showRoomLogin(context, contact);
|
_showRoomLogin(context, contact);
|
||||||
},
|
},
|
||||||
@@ -1180,6 +1629,23 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.my_location),
|
||||||
|
title: Text(context.l10n.map_setAsMyLocation),
|
||||||
|
onTap: () async {
|
||||||
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
|
final successMsg = context.l10n.settings_locationUpdated;
|
||||||
|
Navigator.pop(sheetContext);
|
||||||
|
if (!connector.isConnected) return;
|
||||||
|
await connector.setNodeLocation(
|
||||||
|
lat: position.latitude,
|
||||||
|
lon: position.longitude,
|
||||||
|
);
|
||||||
|
await connector.refreshDeviceInfo();
|
||||||
|
if (!mounted) return;
|
||||||
|
messenger.showSnackBar(SnackBar(content: Text(successMsg)));
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.close),
|
leading: const Icon(Icons.close),
|
||||||
title: Text(context.l10n.common_cancel),
|
title: Text(context.l10n.common_cancel),
|
||||||
@@ -1481,6 +1947,31 @@ 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,
|
||||||
|
),
|
||||||
|
CheckboxListTile(
|
||||||
|
title: Text(context.l10n.map_showDiscoveryContacts),
|
||||||
|
value: settings.mapShowDiscoveryContacts,
|
||||||
|
onChanged: (value) {
|
||||||
|
service.setMapShowDiscoveryContacts(value ?? true);
|
||||||
|
},
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
CheckboxListTile(
|
||||||
|
title: Text(context.l10n.map_showOverlaps),
|
||||||
|
value: settings.mapShowOverlaps,
|
||||||
|
onChanged: (value) {
|
||||||
|
service.setMapShowOverlaps(value ?? true);
|
||||||
|
},
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
context.l10n.map_keyPrefix,
|
context.l10n.map_keyPrefix,
|
||||||
@@ -1639,12 +2130,13 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _startPath() {
|
void _startPath(LatLng position) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isBuildingPathTrace = true;
|
_isBuildingPathTrace = true;
|
||||||
_pathTrace.clear();
|
_pathTrace.clear();
|
||||||
_points.clear();
|
_points.clear();
|
||||||
_polylines.clear();
|
_polylines.clear();
|
||||||
|
_points.add(position);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1690,14 +2182,14 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
.join(','),
|
.join(','),
|
||||||
style: TextStyle(fontSize: 18),
|
style: TextStyle(fontSize: 18),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
// const SizedBox(height: 6),
|
||||||
Wrap(
|
Wrap(
|
||||||
alignment: WrapAlignment.center,
|
alignment: WrapAlignment.center,
|
||||||
spacing: 8,
|
spacing: 1,
|
||||||
runSpacing: 8,
|
runSpacing: 1,
|
||||||
children: [
|
children: [
|
||||||
if (_pathTrace.isNotEmpty)
|
if (_pathTrace.isNotEmpty)
|
||||||
ElevatedButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
@@ -1712,15 +2204,37 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
_isBuildingPathTrace = false;
|
_isBuildingPathTrace = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: Text(l10n.map_runTrace),
|
tooltip: l10n.map_runTrace,
|
||||||
|
icon: const Icon(Icons.arrow_forward_outlined),
|
||||||
),
|
),
|
||||||
if (_pathTrace.isNotEmpty)
|
if (_pathTrace.isNotEmpty)
|
||||||
ElevatedButton(
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PathTraceMapScreen(
|
||||||
|
title: l10n.contacts_pathTrace,
|
||||||
|
path: Uint8List.fromList(_pathTrace),
|
||||||
|
flipPathAround: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_isBuildingPathTrace = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: l10n.map_runTraceWithReturnPath,
|
||||||
|
icon: const Icon(Icons.replay),
|
||||||
|
),
|
||||||
|
if (_pathTrace.isNotEmpty)
|
||||||
|
IconButton(
|
||||||
onPressed: _removePath,
|
onPressed: _removePath,
|
||||||
child: Text(l10n.map_removeLast),
|
tooltip: l10n.map_removeLast,
|
||||||
|
icon: const Icon(Icons.undo),
|
||||||
),
|
),
|
||||||
if (_pathTrace.isEmpty)
|
if (_pathTrace.isEmpty)
|
||||||
ElevatedButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isBuildingPathTrace = false;
|
_isBuildingPathTrace = false;
|
||||||
@@ -1732,7 +2246,8 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
SnackBar(content: Text(l10n.map_pathTraceCancelled)),
|
SnackBar(content: Text(l10n.map_pathTraceCancelled)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Text(l10n.common_cancel),
|
tooltip: l10n.common_cancel,
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1744,6 +2259,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;
|
||||||
|
|||||||
@@ -44,6 +44,24 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
|||||||
PathSelection? _pendingStatusSelection;
|
PathSelection? _pendingStatusSelection;
|
||||||
List<Map<String, dynamic>>? _parsedNeighbors;
|
List<Map<String, dynamic>>? _parsedNeighbors;
|
||||||
|
|
||||||
|
int _resolveRepeaterIndex = -1;
|
||||||
|
|
||||||
|
Contact _resolveRepeater(MeshCoreConnector connector) {
|
||||||
|
if (_resolveRepeaterIndex >= 0 &&
|
||||||
|
_resolveRepeaterIndex < connector.contacts.length &&
|
||||||
|
connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
|
||||||
|
widget.repeater.publicKeyHex) {
|
||||||
|
return connector.contacts[_resolveRepeaterIndex];
|
||||||
|
}
|
||||||
|
_resolveRepeaterIndex = connector.contacts.indexWhere(
|
||||||
|
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
|
||||||
|
);
|
||||||
|
if (_resolveRepeaterIndex == -1) {
|
||||||
|
return widget.repeater;
|
||||||
|
}
|
||||||
|
return connector.contacts[_resolveRepeaterIndex];
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -124,12 +142,11 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
|||||||
|
|
||||||
void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) {
|
void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) {
|
||||||
final buffer = BufferReader(frame);
|
final buffer = BufferReader(frame);
|
||||||
|
final contacts = connector.allContacts;
|
||||||
try {
|
try {
|
||||||
final neighborCount = buffer.readUInt16LE();
|
final neighborCount = buffer.readUInt16LE();
|
||||||
final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE());
|
final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE());
|
||||||
connector.contacts.where((c) => c.type == advTypeRepeater).forEach((
|
contacts.where((c) => c.type == advTypeRepeater).forEach((repeater) {
|
||||||
repeater,
|
|
||||||
) {
|
|
||||||
for (var neighborData in parsedNeighbors) {
|
for (var neighborData in parsedNeighbors) {
|
||||||
final publicKey = neighborData['publicKey'];
|
final publicKey = neighborData['publicKey'];
|
||||||
if (listEquals(
|
if (listEquals(
|
||||||
@@ -164,13 +181,6 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Contact _resolveRepeater(MeshCoreConnector connector) {
|
|
||||||
return connector.contacts.firstWhere(
|
|
||||||
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
|
|
||||||
orElse: () => widget.repeater,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadNeighbors() async {
|
Future<void> _loadNeighbors() async {
|
||||||
if (_commandService == null) return;
|
if (_commandService == null) return;
|
||||||
|
|
||||||
|
|||||||
+228
-31
@@ -52,16 +52,18 @@ class PathTraceMapScreen extends StatefulWidget {
|
|||||||
final String title;
|
final String title;
|
||||||
final Uint8List path;
|
final Uint8List path;
|
||||||
final int? repeaterId;
|
final int? repeaterId;
|
||||||
final bool flipPathRound;
|
final bool flipPathAround;
|
||||||
final bool reversePathRound;
|
final bool reversePathAround;
|
||||||
|
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.repeaterId,
|
||||||
this.flipPathRound = false,
|
this.flipPathAround = false,
|
||||||
this.reversePathRound = false,
|
this.reversePathAround = false,
|
||||||
|
this.targetContact,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -78,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);
|
||||||
@@ -86,6 +93,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
ValueKey<String> _mapKey = const ValueKey('initial');
|
ValueKey<String> _mapKey = const ValueKey('initial');
|
||||||
double _pathDistanceMeters = 0.0;
|
double _pathDistanceMeters = 0.0;
|
||||||
bool _showNodeLabels = true;
|
bool _showNodeLabels = true;
|
||||||
|
Contact? _targetContact;
|
||||||
|
|
||||||
String _formatPathPrefixes(Uint8List pathBytes) {
|
String _formatPathPrefixes(Uint8List pathBytes) {
|
||||||
return pathBytes
|
return pathBytes
|
||||||
@@ -107,8 +115,30 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint8List addReturnPath(Uint8List pathBytes) {
|
Uint8List buildPath(Uint8List pathBytes) {
|
||||||
Uint8List? traceBytes;
|
Uint8List traceBytes;
|
||||||
|
|
||||||
|
if (pathBytes.isEmpty) {
|
||||||
|
traceBytes = Uint8List(1);
|
||||||
|
traceBytes[0] = widget.targetContact?.publicKey[0] ?? 0;
|
||||||
|
return traceBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget.targetContact?.type == advTypeRepeater ||
|
||||||
|
widget.targetContact?.type == advTypeRoom) {
|
||||||
|
final len = (pathBytes.length + pathBytes.length + 1);
|
||||||
|
traceBytes = Uint8List(len);
|
||||||
|
traceBytes[pathBytes.length] = widget.targetContact?.publicKey[0] ?? 0;
|
||||||
|
for (int i = 0; i < pathBytes.length; i++) {
|
||||||
|
traceBytes[i] = pathBytes[i];
|
||||||
|
if (i < pathBytes.length) {
|
||||||
|
traceBytes[len - 1 - i] = pathBytes[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (pathBytes.length < 2) {
|
||||||
|
return pathBytes[0] == 0 ? Uint8List(0) : pathBytes;
|
||||||
|
}
|
||||||
final len = (pathBytes.length + pathBytes.length - 1);
|
final len = (pathBytes.length + pathBytes.length - 1);
|
||||||
traceBytes = Uint8List(len);
|
traceBytes = Uint8List(len);
|
||||||
for (int i = 0; i < pathBytes.length; i++) {
|
for (int i = 0; i < pathBytes.length; i++) {
|
||||||
@@ -117,6 +147,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
traceBytes[len - 1 - i] = pathBytes[i];
|
traceBytes[len - 1 - i] = pathBytes[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return traceBytes;
|
return traceBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,17 +159,17 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final Uint8List path;
|
final pathTmp = widget.reversePathAround
|
||||||
|
|
||||||
Uint8List pathTmp = widget.reversePathRound
|
|
||||||
? Uint8List.fromList(widget.path.reversed.toList())
|
? Uint8List.fromList(widget.path.reversed.toList())
|
||||||
: widget.path;
|
: widget.path;
|
||||||
|
|
||||||
if (widget.flipPathRound) {
|
final path = widget.flipPathAround ? buildPath(pathTmp) : pathTmp;
|
||||||
path = addReturnPath(pathTmp);
|
|
||||||
} else {
|
appLogger.info(
|
||||||
path = pathTmp;
|
'Initiating path trace with path: ${_formatPathPrefixes(path)}',
|
||||||
}
|
tag: 'PathTraceMapScreen',
|
||||||
|
noNotify: !mounted,
|
||||||
|
);
|
||||||
|
|
||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
final frame = buildTraceReq(
|
final frame = buildTraceReq(
|
||||||
@@ -228,10 +259,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
Map<int, Contact> pathContacts = {};
|
Map<int, Contact> pathContacts = {};
|
||||||
|
final contacts = connector.allContacts;
|
||||||
connector.contacts.where((c) => c.type != advTypeChat).forEach((
|
contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
|
||||||
repeater,
|
|
||||||
) {
|
|
||||||
for (var repeaterData in pathData) {
|
for (var repeaterData in pathData) {
|
||||||
if (listEquals(
|
if (listEquals(
|
||||||
repeater.publicKey.sublist(0, 1),
|
repeater.publicKey.sublist(0, 1),
|
||||||
@@ -242,23 +271,125 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// For hops with no GPS contact, infer position from other contacts
|
||||||
|
// with known GPS that share the same last-hop byte.
|
||||||
|
final Map<int, LatLng> inferredPositions = {};
|
||||||
|
for (final hop in pathData) {
|
||||||
|
final contact = pathContacts[hop];
|
||||||
|
if (contact != null && contact.hasLocation) continue;
|
||||||
|
final peers = connector.contacts
|
||||||
|
.where(
|
||||||
|
(c) => c.hasLocation && c.path.isNotEmpty && c.path.last == hop,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
if (peers.isNotEmpty) {
|
||||||
|
final lat =
|
||||||
|
peers.map((c) => c.latitude!).reduce((a, b) => a + b) /
|
||||||
|
peers.length;
|
||||||
|
final lon =
|
||||||
|
peers.map((c) => c.longitude!).reduce((a, b) => a + b) /
|
||||||
|
peers.length;
|
||||||
|
inferredPositions[hop] = LatLng(lat, lon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
_hasData = true;
|
_hasData = true;
|
||||||
|
_inferredHopPositions = inferredPositions;
|
||||||
_traceData = PathTraceData(
|
_traceData = PathTraceData(
|
||||||
pathData: pathData,
|
pathData: pathData,
|
||||||
snrData: snrData,
|
snrData: snrData,
|
||||||
pathContacts: pathContacts,
|
pathContacts: pathContacts,
|
||||||
);
|
);
|
||||||
|
// Compute endpoint position for the target contact.
|
||||||
|
LatLng? targetPos;
|
||||||
|
bool targetGuessed = false;
|
||||||
|
_targetContact = widget.targetContact;
|
||||||
|
|
||||||
|
if (_targetContact != null) {
|
||||||
|
final tc = _targetContact!;
|
||||||
|
if (tc.hasLocation) {
|
||||||
|
targetPos = LatLng(tc.latitude!, tc.longitude!);
|
||||||
|
} else if (widget.path.length > 1) {
|
||||||
|
// Infer from the last hop: average GPS contacts sharing that hop.
|
||||||
|
// For a round-trip path (flipPathAround/reversePathAround), the target-side hop
|
||||||
|
// sits in the middle of the symmetric sequence; .last is the local side.
|
||||||
|
final lastHop = widget.reversePathAround
|
||||||
|
? widget.path.first
|
||||||
|
: widget.path.last;
|
||||||
|
|
||||||
|
final peers = connector.allContacts
|
||||||
|
.where(
|
||||||
|
(c) =>
|
||||||
|
c.hasLocation &&
|
||||||
|
c.path.isNotEmpty &&
|
||||||
|
c.path.last == lastHop,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
if (peers.isNotEmpty) {
|
||||||
|
final lat =
|
||||||
|
peers.map((c) => c.latitude!).reduce((a, b) => a + b) /
|
||||||
|
peers.length;
|
||||||
|
final lon =
|
||||||
|
peers.map((c) => c.longitude!).reduce((a, b) => a + b) /
|
||||||
|
peers.length;
|
||||||
|
const offsetDeg = 0.003;
|
||||||
|
final angle = (tc.publicKey[1] / 255.0) * 2 * pi;
|
||||||
|
targetPos = LatLng(
|
||||||
|
lat + offsetDeg * cos(angle),
|
||||||
|
lon + offsetDeg * sin(angle),
|
||||||
|
);
|
||||||
|
targetGuessed = true;
|
||||||
|
} else if (inferredPositions.containsKey(lastHop)) {
|
||||||
|
final lat = inferredPositions[lastHop]!.latitude;
|
||||||
|
final lon = inferredPositions[lastHop]!.longitude;
|
||||||
|
const offsetDeg = 0.003;
|
||||||
|
final angle = (tc.publicKey[1] / 255.0) * 2 * pi;
|
||||||
|
targetPos = LatLng(
|
||||||
|
lat + offsetDeg * cos(angle),
|
||||||
|
lon + offsetDeg * sin(angle),
|
||||||
|
);
|
||||||
|
targetGuessed = true;
|
||||||
|
} else {
|
||||||
|
// As a last resort, just place it at the same position as the last hop.
|
||||||
|
final contact = pathContacts[lastHop];
|
||||||
|
if (contact != null && contact.hasLocation) {
|
||||||
|
const offsetDeg = 0.003;
|
||||||
|
final angle = (tc.publicKey[1] / 255.0) * 2 * pi;
|
||||||
|
targetPos = LatLng(
|
||||||
|
contact.latitude! + offsetDeg * cos(angle),
|
||||||
|
contact.longitude! + offsetDeg * sin(angle),
|
||||||
|
);
|
||||||
|
targetGuessed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_targetContactPosition = targetPos;
|
||||||
|
_targetContactIsGuessed = targetGuessed;
|
||||||
|
|
||||||
_points = <LatLng>[];
|
_points = <LatLng>[];
|
||||||
_points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
_points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
||||||
|
int hopLast = 0;
|
||||||
|
int hopLastLast = 0;
|
||||||
for (final hop in _traceData!.pathData) {
|
for (final hop in _traceData!.pathData) {
|
||||||
|
if (hop == hopLastLast && widget.flipPathAround) {
|
||||||
|
break; //skip duplicate hops in round-trip paths
|
||||||
|
}
|
||||||
final contact = _traceData!.pathContacts[hop];
|
final contact = _traceData!.pathContacts[hop];
|
||||||
if (contact != null &&
|
if (contact != null && contact.hasLocation) {
|
||||||
contact.hasLocation &&
|
|
||||||
contact.latitude != null &&
|
|
||||||
contact.longitude != null) {
|
|
||||||
_points.add(LatLng(contact.latitude!, contact.longitude!));
|
_points.add(LatLng(contact.latitude!, contact.longitude!));
|
||||||
|
} else {
|
||||||
|
final inferred = inferredPositions[hop];
|
||||||
|
if (inferred != null) _points.add(inferred);
|
||||||
|
}
|
||||||
|
hopLastLast = hopLast;
|
||||||
|
hopLast = hop;
|
||||||
|
}
|
||||||
|
if (targetPos != null) {
|
||||||
|
if (_targetContact != null && _targetContact!.type == advTypeChat) {
|
||||||
|
_points.add(targetPos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_polylines = _points.length > 1
|
_polylines = _points.length > 1
|
||||||
@@ -349,7 +480,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_hasData) _buildMapPathTrace(context, tileCache),
|
if (_hasData)
|
||||||
|
_buildMapPathTrace(context, tileCache, _targetContact),
|
||||||
if (_points.isEmpty &&
|
if (_points.isEmpty &&
|
||||||
!_hasData &&
|
!_hasData &&
|
||||||
!_isLoading &&
|
!_isLoading &&
|
||||||
@@ -378,12 +510,28 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
List<Marker> _buildHopMarkers(
|
List<Marker> _buildHopMarkers(
|
||||||
List<int> pathData, {
|
List<int> pathData, {
|
||||||
required bool showLabels,
|
required bool showLabels,
|
||||||
|
required Contact? target,
|
||||||
}) {
|
}) {
|
||||||
final markers = <Marker>[];
|
final markers = <Marker>[];
|
||||||
|
int hopLast = 0;
|
||||||
|
int hopLastLast = 0;
|
||||||
for (final hop in pathData) {
|
for (final hop in pathData) {
|
||||||
final contact = _traceData!.pathContacts[hop];
|
final contact = _traceData!.pathContacts[hop];
|
||||||
if (contact == null || !contact.hasLocation) continue;
|
final inferred = _inferredHopPositions[hop];
|
||||||
final point = LatLng(contact.latitude!, contact.longitude!);
|
final hasGps = contact != null && contact.hasLocation;
|
||||||
|
if (hop == hopLastLast && widget.flipPathAround) {
|
||||||
|
continue; //skip duplicate hops in round-trip paths
|
||||||
|
}
|
||||||
|
if (!hasGps && inferred == null) {
|
||||||
|
hopLastLast = hopLast;
|
||||||
|
hopLast = hop;
|
||||||
|
continue; //skip hops with no GPS and no inferred position
|
||||||
|
}
|
||||||
|
final point = hasGps
|
||||||
|
? LatLng(contact.latitude!, contact.longitude!)
|
||||||
|
: inferred!;
|
||||||
|
final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||||
|
|
||||||
markers.add(
|
markers.add(
|
||||||
Marker(
|
Marker(
|
||||||
point: point,
|
point: point,
|
||||||
@@ -392,7 +540,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.green,
|
color: hasGps
|
||||||
|
? Colors.green
|
||||||
|
: Colors.orange.withValues(alpha: 0.75),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
border: Border.all(color: Colors.white, width: 2),
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
@@ -405,10 +555,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
),
|
),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Text(
|
child: Text(
|
||||||
contact.publicKey
|
hasGps ? label : '~$label',
|
||||||
.sublist(0, 1)
|
|
||||||
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
|
||||||
.join(),
|
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -419,8 +566,15 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (showLabels) {
|
if (showLabels) {
|
||||||
markers.add(_buildNodeLabelMarker(point: point, label: contact.name));
|
markers.add(
|
||||||
|
_buildNodeLabelMarker(
|
||||||
|
point: point,
|
||||||
|
label: contact?.name ?? '~$label',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
hopLastLast = hopLast;
|
||||||
|
hopLast = hop;
|
||||||
}
|
}
|
||||||
|
|
||||||
final selfLat = context.read<MeshCoreConnector>().selfLatitude;
|
final selfLat = context.read<MeshCoreConnector>().selfLatitude;
|
||||||
@@ -468,6 +622,47 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add target contact endpoint marker.
|
||||||
|
final targetPos = _targetContactPosition;
|
||||||
|
if (targetPos != null && target != null && target.type == advTypeChat) {
|
||||||
|
final isGuessed = _targetContactIsGuessed;
|
||||||
|
final targetName = target.name;
|
||||||
|
markers.add(
|
||||||
|
Marker(
|
||||||
|
point: targetPos,
|
||||||
|
width: 35,
|
||||||
|
height: 35,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isGuessed
|
||||||
|
? Colors.purple.withValues(alpha: 0.55)
|
||||||
|
: Colors.red,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.3),
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: const Icon(Icons.person, color: Colors.white, size: 18),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (showLabels) {
|
||||||
|
markers.add(
|
||||||
|
_buildNodeLabelMarker(
|
||||||
|
point: targetPos,
|
||||||
|
label: isGuessed ? '~$targetName' : targetName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return markers;
|
return markers;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,6 +762,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
Widget _buildMapPathTrace(
|
Widget _buildMapPathTrace(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
MapTileCacheService tileCache,
|
MapTileCacheService tileCache,
|
||||||
|
Contact? target,
|
||||||
) {
|
) {
|
||||||
return FlutterMap(
|
return FlutterMap(
|
||||||
key: _mapKey,
|
key: _mapKey,
|
||||||
@@ -605,6 +801,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
markers: _buildHopMarkers(
|
markers: _buildHopMarkers(
|
||||||
_traceData!.pathData,
|
_traceData!.pathData,
|
||||||
showLabels: _showNodeLabels,
|
showLabels: _showNodeLabels,
|
||||||
|
target: target,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -77,11 +77,22 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int _resolveRepeaterIndex = -1;
|
||||||
|
|
||||||
Contact _resolveRepeater(MeshCoreConnector connector) {
|
Contact _resolveRepeater(MeshCoreConnector connector) {
|
||||||
return connector.contacts.firstWhere(
|
if (_resolveRepeaterIndex >= 0 &&
|
||||||
|
_resolveRepeaterIndex < connector.contacts.length &&
|
||||||
|
connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
|
||||||
|
widget.repeater.publicKeyHex) {
|
||||||
|
return connector.contacts[_resolveRepeaterIndex];
|
||||||
|
}
|
||||||
|
_resolveRepeaterIndex = connector.contacts.indexWhere(
|
||||||
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
|
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
|
||||||
orElse: () => widget.repeater,
|
|
||||||
);
|
);
|
||||||
|
if (_resolveRepeaterIndex == -1) {
|
||||||
|
return widget.repeater;
|
||||||
|
}
|
||||||
|
return connector.contacts[_resolveRepeaterIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleTextMessageResponse(Uint8List frame) {
|
void _handleTextMessageResponse(Uint8List frame) {
|
||||||
|
|||||||
@@ -205,8 +205,7 @@ class RepeaterHubScreen extends StatelessWidget {
|
|||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) =>
|
builder: (context) => TelemetryScreen(contact: repeater),
|
||||||
TelemetryScreen(repeater: repeater, password: password),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -129,11 +129,22 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
|||||||
_commandService?.handleResponse(widget.repeater, parsed.text);
|
_commandService?.handleResponse(widget.repeater, parsed.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int _resolveRepeaterIndex = -1;
|
||||||
|
|
||||||
Contact _resolveRepeater(MeshCoreConnector connector) {
|
Contact _resolveRepeater(MeshCoreConnector connector) {
|
||||||
return connector.contacts.firstWhere(
|
if (_resolveRepeaterIndex >= 0 &&
|
||||||
|
_resolveRepeaterIndex < connector.contacts.length &&
|
||||||
|
connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
|
||||||
|
widget.repeater.publicKeyHex) {
|
||||||
|
return connector.contacts[_resolveRepeaterIndex];
|
||||||
|
}
|
||||||
|
_resolveRepeaterIndex = connector.contacts.indexWhere(
|
||||||
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
|
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
|
||||||
orElse: () => widget.repeater,
|
|
||||||
);
|
);
|
||||||
|
if (_resolveRepeaterIndex == -1) {
|
||||||
|
return widget.repeater;
|
||||||
|
}
|
||||||
|
return connector.contacts[_resolveRepeaterIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _matchesRepeaterPrefix(Uint8List prefix) {
|
bool _matchesRepeaterPrefix(Uint8List prefix) {
|
||||||
|
|||||||
@@ -91,11 +91,22 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int _resolveRepeaterIndex = -1;
|
||||||
|
|
||||||
Contact _resolveRepeater(MeshCoreConnector connector) {
|
Contact _resolveRepeater(MeshCoreConnector connector) {
|
||||||
return connector.contacts.firstWhere(
|
if (_resolveRepeaterIndex >= 0 &&
|
||||||
|
_resolveRepeaterIndex < connector.contacts.length &&
|
||||||
|
connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
|
||||||
|
widget.repeater.publicKeyHex) {
|
||||||
|
return connector.contacts[_resolveRepeaterIndex];
|
||||||
|
}
|
||||||
|
_resolveRepeaterIndex = connector.contacts.indexWhere(
|
||||||
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
|
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
|
||||||
orElse: () => widget.repeater,
|
|
||||||
);
|
);
|
||||||
|
if (_resolveRepeaterIndex == -1) {
|
||||||
|
return widget.repeater;
|
||||||
|
}
|
||||||
|
return connector.contacts[_resolveRepeaterIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleTextMessageResponse(Uint8List frame) {
|
void _handleTextMessageResponse(Uint8List frame) {
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
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/adaptive_app_bar_title.dart';
|
||||||
import '../widgets/device_tile.dart';
|
import '../widgets/device_tile.dart';
|
||||||
import 'contacts_screen.dart';
|
import 'contacts_screen.dart';
|
||||||
|
import 'tcp_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 {
|
||||||
@@ -21,6 +23,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;
|
||||||
@@ -28,12 +31,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) {
|
||||||
@@ -44,33 +50,52 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
connector.addListener(_connectionListener);
|
_connector.addListener(_connectionListener);
|
||||||
|
|
||||||
_bluetoothStateSubscription = FlutterBluePlus.adapterState.listen((state) {
|
_bluetoothStateSubscription = FlutterBluePlus.adapterState.listen(
|
||||||
|
(state) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_bluetoothState = state;
|
_bluetoothState = state;
|
||||||
});
|
});
|
||||||
// Cancel scan if Bluetooth turns off while scanning
|
// Cancel scan if Bluetooth turns off while scanning
|
||||||
if (state != BluetoothAdapterState.on) {
|
if (state != BluetoothAdapterState.on) {
|
||||||
unawaited(connector.stopScan());
|
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(
|
||||||
|
leading: canPop
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () {
|
||||||
|
appLogger.info('Back button pressed', tag: 'ScannerScreen');
|
||||||
|
Navigator.of(context).maybePop();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
title: AdaptiveAppBarTitle(context.l10n.scanner_title),
|
title: AdaptiveAppBarTitle(context.l10n.scanner_title),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
@@ -95,30 +120,74 @@ 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;
|
||||||
|
final tcpSupported = !PlatformInfo.isWeb;
|
||||||
|
|
||||||
return FloatingActionButton.extended(
|
return SafeArea(
|
||||||
|
top: false,
|
||||||
|
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
if (usbSupported)
|
||||||
|
FloatingActionButton.extended(
|
||||||
|
onPressed: () {
|
||||||
|
appLogger.info(
|
||||||
|
'USB selected, opening UsbScreen',
|
||||||
|
tag: 'ScannerScreen',
|
||||||
|
);
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(builder: (_) => const UsbScreen()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
heroTag: 'scanner_usb_action',
|
||||||
|
icon: const Icon(Icons.usb),
|
||||||
|
label: Text(context.l10n.connectionChoiceUsbLabel),
|
||||||
|
),
|
||||||
|
if (usbSupported) const SizedBox(width: 12),
|
||||||
|
if (tcpSupported)
|
||||||
|
FloatingActionButton.extended(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(builder: (_) => const TcpScreen()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
heroTag: 'scanner_tcp_action',
|
||||||
|
icon: const Icon(Icons.lan),
|
||||||
|
label: Text(context.l10n.connectionChoiceTcpLabel),
|
||||||
|
),
|
||||||
|
if (tcpSupported) const SizedBox(width: 12),
|
||||||
|
FloatingActionButton.extended(
|
||||||
|
heroTag: 'scanner_ble_action',
|
||||||
onPressed: isBluetoothOff
|
onPressed: isBluetoothOff
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
if (isScanning) {
|
if (isScanning) {
|
||||||
connector.stopScan();
|
connector.stopScan();
|
||||||
} else {
|
} else {
|
||||||
connector.startScan();
|
unawaited(
|
||||||
|
connector.startScan().catchError((e) {
|
||||||
|
appLogger.warn(
|
||||||
|
'startScan error: $e',
|
||||||
|
tag: 'ScannerScreen',
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: isScanning
|
icon: isScanning
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 20,
|
width: 20,
|
||||||
height: 20,
|
height: 20,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
strokeWidth: 2,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
: const Icon(Icons.bluetooth_searching),
|
: const Icon(Icons.bluetooth_searching),
|
||||||
label: Text(
|
label: Text(
|
||||||
@@ -126,6 +195,10 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
|||||||
? context.l10n.scanner_stop
|
? context.l10n.scanner_stop
|
||||||
: context.l10n.scanner_scan,
|
: context.l10n.scanner_scan,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -265,7 +338,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,7 +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/adaptive_app_bar_title.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';
|
||||||
@@ -43,8 +43,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: AdaptiveAppBarTitle(l10n.settings_title),
|
title: AppBarTitle(
|
||||||
centerTitle: true,
|
l10n.settings_title,
|
||||||
|
indicators: false,
|
||||||
|
subtitle: false,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
@@ -275,11 +278,19 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.visibility_off_outlined),
|
leading: const Icon(Icons.group_add_outlined),
|
||||||
title: Text(l10n.settings_privacyMode),
|
title: Text(l10n.settings_contactSettings),
|
||||||
subtitle: Text(l10n.settings_privacyModeSubtitle),
|
subtitle: Text(l10n.settings_contactSettingsSubtitle),
|
||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
onTap: () => _togglePrivacy(context, connector),
|
onTap: () => _editAutoAddConfig(context, connector),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.visibility_off_outlined),
|
||||||
|
title: Text(l10n.settings_privacy),
|
||||||
|
subtitle: Text(l10n.settings_privacySubtitle),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () => _privacySettings(context, connector),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -300,10 +311,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.cell_tower),
|
leading: const Icon(Icons.delete_outline, color: Colors.red),
|
||||||
title: Text(l10n.settings_sendAdvertisement),
|
title: Text("Delete All Paths"),
|
||||||
subtitle: Text(l10n.settings_sendAdvertisementSubtitle),
|
subtitle: Text(
|
||||||
onTap: () => _sendAdvert(context, connector),
|
"Clear all path data from contacts.",
|
||||||
|
style: TextStyle(color: Colors.red[700]),
|
||||||
|
),
|
||||||
|
onTap: () => connector.deleteAllPaths(),
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -646,55 +660,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _togglePrivacy(BuildContext context, MeshCoreConnector connector) {
|
|
||||||
final l10n = context.l10n;
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: Text(l10n.settings_privacyMode),
|
|
||||||
content: Text(l10n.settings_privacyModeToggle),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: Text(l10n.common_cancel),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
Navigator.pop(context);
|
|
||||||
await connector.setPrivacyMode(true);
|
|
||||||
await connector.refreshDeviceInfo();
|
|
||||||
if (!context.mounted) return;
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text(l10n.settings_privacyModeEnabled)),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Text(l10n.common_enable),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
Navigator.pop(context);
|
|
||||||
await connector.setPrivacyMode(false);
|
|
||||||
await connector.refreshDeviceInfo();
|
|
||||||
if (!context.mounted) return;
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text(l10n.settings_privacyModeDisabled)),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Text(l10n.common_disable),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _sendAdvert(BuildContext context, MeshCoreConnector connector) {
|
|
||||||
final l10n = context.l10n;
|
|
||||||
connector.sendSelfAdvert(flood: true);
|
|
||||||
ScaffoldMessenger.of(
|
|
||||||
context,
|
|
||||||
).showSnackBar(SnackBar(content: Text(l10n.settings_advertisementSent)));
|
|
||||||
}
|
|
||||||
|
|
||||||
void _syncTime(BuildContext context, MeshCoreConnector connector) {
|
void _syncTime(BuildContext context, MeshCoreConnector connector) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
connector.syncTime();
|
connector.syncTime();
|
||||||
@@ -849,6 +814,251 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _editAutoAddConfig(BuildContext context, MeshCoreConnector connector) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
bool autoAddChat = false;
|
||||||
|
bool autoAddRepeater = false;
|
||||||
|
bool autoAddRoomServer = false;
|
||||||
|
bool autoAddSensor = false;
|
||||||
|
bool overwriteOldest = false;
|
||||||
|
|
||||||
|
final connector = context.read<MeshCoreConnector>();
|
||||||
|
autoAddChat = connector.autoAddUsers ?? false;
|
||||||
|
autoAddRepeater = connector.autoAddRepeaters ?? false;
|
||||||
|
autoAddRoomServer = connector.autoAddRoomServers ?? false;
|
||||||
|
autoAddSensor = connector.autoAddSensors ?? false;
|
||||||
|
overwriteOldest = connector.autoAddOverwriteOldest ?? false;
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) => StatefulBuilder(
|
||||||
|
builder: (context, setDialogState) => AlertDialog(
|
||||||
|
title: Text(l10n.contactsSettings_autoAddTitle),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
FeatureToggleRow(
|
||||||
|
title: l10n.contactsSettings_autoAddUsersTitle,
|
||||||
|
subtitle: l10n.contactsSettings_autoAddUsersSubtitle,
|
||||||
|
value: autoAddChat,
|
||||||
|
onChanged: (value) {
|
||||||
|
setDialogState(() => autoAddChat = value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
FeatureToggleRow(
|
||||||
|
title: l10n.contactsSettings_autoAddRepeatersTitle,
|
||||||
|
subtitle: l10n.contactsSettings_autoAddRepeatersSubtitle,
|
||||||
|
value: autoAddRepeater,
|
||||||
|
onChanged: (value) {
|
||||||
|
setDialogState(() => autoAddRepeater = value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
FeatureToggleRow(
|
||||||
|
title: l10n.contactsSettings_autoAddRoomServersTitle,
|
||||||
|
subtitle: l10n.contactsSettings_autoAddRoomServersSubtitle,
|
||||||
|
value: autoAddRoomServer,
|
||||||
|
onChanged: (value) {
|
||||||
|
setDialogState(() => autoAddRoomServer = value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
FeatureToggleRow(
|
||||||
|
title: l10n.contactsSettings_autoAddSensorsTitle,
|
||||||
|
subtitle: l10n.contactsSettings_autoAddSensorsSubtitle,
|
||||||
|
value: autoAddSensor,
|
||||||
|
onChanged: (value) {
|
||||||
|
setDialogState(() => autoAddSensor = value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Divider(height: 4),
|
||||||
|
FeatureToggleRow(
|
||||||
|
title: l10n.contactsSettings_overwriteOldestTitle,
|
||||||
|
subtitle: l10n.contactsSettings_overwriteOldestSubtitle,
|
||||||
|
value: overwriteOldest,
|
||||||
|
onChanged: (value) {
|
||||||
|
setDialogState(() => overwriteOldest = value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(l10n.common_cancel),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
_sendSettings(
|
||||||
|
connector,
|
||||||
|
autoAddChat,
|
||||||
|
autoAddRepeater,
|
||||||
|
autoAddRoomServer,
|
||||||
|
autoAddSensor,
|
||||||
|
overwriteOldest,
|
||||||
|
);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Text(l10n.common_save),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _sendSettings(
|
||||||
|
MeshCoreConnector connector,
|
||||||
|
bool autoAddChat,
|
||||||
|
bool autoAddRepeater,
|
||||||
|
bool autoAddRoomServer,
|
||||||
|
bool autoAddSensor,
|
||||||
|
bool overwriteOldest,
|
||||||
|
) async {
|
||||||
|
final frame = buildSetAutoAddConfigFrame(
|
||||||
|
autoAddChat: autoAddChat,
|
||||||
|
autoAddRepeater: autoAddRepeater,
|
||||||
|
autoAddRoomServer: autoAddRoomServer,
|
||||||
|
autoAddSensor: autoAddSensor,
|
||||||
|
overwriteOldest: overwriteOldest,
|
||||||
|
);
|
||||||
|
await connector.sendFrame(frame);
|
||||||
|
await connector.sendFrame(buildGetAutoAddFlagsFrame());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _privacySettings(BuildContext context, MeshCoreConnector connector) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
|
||||||
|
int telemetryMode = connector.telemetryModeBase;
|
||||||
|
int telemetryLocMode = connector.telemetryModeLoc;
|
||||||
|
int telemetryEnvMode = connector.telemetryModeEnv;
|
||||||
|
bool advertLocPolicy = connector.advertLocationPolicy == 0 ? false : true;
|
||||||
|
int multiAcks = connector.multiAcks;
|
||||||
|
|
||||||
|
final telemModeBase = [
|
||||||
|
DropdownMenuItem(value: teleModeDeny, child: Text(l10n.settings_denyAll)),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: teleModeAllowFlags,
|
||||||
|
child: Text(l10n.settings_allowByContact),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: teleModeAllowAll,
|
||||||
|
child: Text(l10n.settings_allowAll),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) => StatefulBuilder(
|
||||||
|
builder: (context, setDialogState) => AlertDialog(
|
||||||
|
title: Text(l10n.settings_privacy),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(l10n.settings_privacySettingsDescription),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
FeatureToggleRow(
|
||||||
|
title: l10n.settings_advertLocation,
|
||||||
|
subtitle: l10n.settings_advertLocationSubtitle,
|
||||||
|
value: advertLocPolicy,
|
||||||
|
onChanged: (value) {
|
||||||
|
setDialogState(() => advertLocPolicy = value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
DropdownButtonFormField<int>(
|
||||||
|
initialValue: telemetryMode,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: l10n.settings_telemetryBaseMode,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
items: telemModeBase,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
setDialogState(() => telemetryMode = value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
DropdownButtonFormField<int>(
|
||||||
|
initialValue: telemetryLocMode,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: l10n.settings_telemetryLocationMode,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
items: telemModeBase,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
setDialogState(() => telemetryLocMode = value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
DropdownButtonFormField<int>(
|
||||||
|
initialValue: telemetryEnvMode,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: l10n.settings_telemetryEnvironmentMode,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
items: telemModeBase,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
setDialogState(() => telemetryEnvMode = value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
l10n.settings_multiAck(multiAcks.toString()),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
Slider(
|
||||||
|
value: multiAcks.toDouble(),
|
||||||
|
min: 0,
|
||||||
|
max: 2,
|
||||||
|
divisions: 2,
|
||||||
|
label: multiAcks.toString(),
|
||||||
|
onChanged: (value) {
|
||||||
|
setDialogState(() => multiAcks = value.round());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(l10n.common_cancel),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.pop(context);
|
||||||
|
await connector.setTelemetryModeBase(
|
||||||
|
telemetryMode,
|
||||||
|
telemetryLocMode,
|
||||||
|
telemetryEnvMode,
|
||||||
|
advertLocPolicy ? 1 : 0,
|
||||||
|
multiAcks,
|
||||||
|
);
|
||||||
|
await connector.refreshDeviceInfo();
|
||||||
|
if (!context.mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(l10n.settings_telemetryModeUpdated)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text(l10n.common_save),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RadioSettingsDialog extends StatefulWidget {
|
class _RadioSettingsDialog extends StatefulWidget {
|
||||||
|
|||||||
@@ -0,0 +1,295 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../connector/meshcore_connector.dart';
|
||||||
|
import '../l10n/l10n.dart';
|
||||||
|
import '../services/app_settings_service.dart';
|
||||||
|
import '../utils/platform_info.dart';
|
||||||
|
import '../widgets/adaptive_app_bar_title.dart';
|
||||||
|
import 'contacts_screen.dart';
|
||||||
|
import 'usb_screen.dart';
|
||||||
|
|
||||||
|
class TcpScreen extends StatefulWidget {
|
||||||
|
const TcpScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TcpScreen> createState() => _TcpScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TcpScreenState extends State<TcpScreen> {
|
||||||
|
late final TextEditingController _hostController;
|
||||||
|
late final TextEditingController _portController;
|
||||||
|
late final MeshCoreConnector _connector;
|
||||||
|
late final VoidCallback _connectionListener;
|
||||||
|
bool _navigatedToContacts = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_hostController = TextEditingController(
|
||||||
|
text: context.read<AppSettingsService>().settings.tcpServerAddress,
|
||||||
|
);
|
||||||
|
_portController = TextEditingController(
|
||||||
|
text: context.read<AppSettingsService>().settings.tcpServerPort > 0
|
||||||
|
? context.read<AppSettingsService>().settings.tcpServerPort.toString()
|
||||||
|
: '',
|
||||||
|
);
|
||||||
|
_connector = context.read<MeshCoreConnector>();
|
||||||
|
|
||||||
|
_connectionListener = () {
|
||||||
|
if (!mounted) return;
|
||||||
|
if (_connector.state == MeshCoreConnectionState.disconnected) {
|
||||||
|
_navigatedToContacts = false;
|
||||||
|
}
|
||||||
|
if (_connector.state == MeshCoreConnectionState.connected &&
|
||||||
|
_connector.isTcpTransportConnected &&
|
||||||
|
!_navigatedToContacts) {
|
||||||
|
context.read<AppSettingsService>().setTcpServerAddress(
|
||||||
|
_hostController.text,
|
||||||
|
);
|
||||||
|
context.read<AppSettingsService>().setTcpServerPort(
|
||||||
|
int.tryParse(_portController.text) ?? 0,
|
||||||
|
);
|
||||||
|
_navigatedToContacts = true;
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(builder: (_) => const ContactsScreen()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_connector.addListener(_connectionListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_hostController.dispose();
|
||||||
|
_portController.dispose();
|
||||||
|
_connector.removeListener(_connectionListener);
|
||||||
|
if (!_navigatedToContacts &&
|
||||||
|
_connector.activeTransport == MeshCoreTransportType.tcp &&
|
||||||
|
_connector.state != MeshCoreConnectionState.disconnected) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
unawaited(_connector.disconnect(manual: true));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => Navigator.of(context).maybePop(),
|
||||||
|
),
|
||||||
|
title: AdaptiveAppBarTitle(context.l10n.tcpScreenTitle),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: Consumer<MeshCoreConnector>(
|
||||||
|
builder: (context, connector, child) {
|
||||||
|
final isConnecting =
|
||||||
|
connector.state == MeshCoreConnectionState.connecting &&
|
||||||
|
connector.activeTransport == MeshCoreTransportType.tcp;
|
||||||
|
final isButtonDisabled =
|
||||||
|
isConnecting ||
|
||||||
|
connector.state == MeshCoreConnectionState.scanning;
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_buildStatusBar(context, connector),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _hostController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: context.l10n.tcpHostLabel,
|
||||||
|
hintText: context.l10n.tcpHostHint,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
enabled: !isConnecting,
|
||||||
|
keyboardType: TextInputType.url,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextField(
|
||||||
|
controller: _portController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: context.l10n.tcpPortLabel,
|
||||||
|
hintText: context.l10n.tcpPortHint,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
enabled: !isConnecting,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
FilledButton.icon(
|
||||||
|
key: const Key('tcp_connect_button'),
|
||||||
|
onPressed: isButtonDisabled ? null : _connectTcp,
|
||||||
|
icon: isConnecting
|
||||||
|
? const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.lan),
|
||||||
|
label: Text(
|
||||||
|
isConnecting
|
||||||
|
? context.l10n.scanner_connecting
|
||||||
|
: context.l10n.common_connect,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bottomNavigationBar: SafeArea(
|
||||||
|
top: false,
|
||||||
|
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
if (PlatformInfo.supportsUsbSerial)
|
||||||
|
FloatingActionButton.extended(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(builder: (_) => const UsbScreen()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
heroTag: 'tcp_usb_action',
|
||||||
|
extendedPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
icon: const Icon(Icons.usb),
|
||||||
|
label: Text(context.l10n.connectionChoiceUsbLabel),
|
||||||
|
),
|
||||||
|
if (PlatformInfo.supportsUsbSerial) const SizedBox(width: 12),
|
||||||
|
FloatingActionButton.extended(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).maybePop();
|
||||||
|
},
|
||||||
|
heroTag: 'tcp_ble_action',
|
||||||
|
extendedPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
icon: const Icon(Icons.bluetooth),
|
||||||
|
label: Text(context.l10n.connectionChoiceBluetoothLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatusBar(BuildContext context, MeshCoreConnector connector) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
String statusText;
|
||||||
|
Color statusColor;
|
||||||
|
|
||||||
|
if (connector.isTcpTransportConnected) {
|
||||||
|
statusText = l10n.scanner_connectedTo(
|
||||||
|
connector.activeTcpEndpoint ?? 'TCP',
|
||||||
|
);
|
||||||
|
statusColor = Colors.green;
|
||||||
|
} else if (connector.state == MeshCoreConnectionState.connecting &&
|
||||||
|
connector.activeTransport == MeshCoreTransportType.tcp) {
|
||||||
|
statusText = l10n.tcpStatus_connectingTo(
|
||||||
|
'${_hostController.text}:${_portController.text}',
|
||||||
|
);
|
||||||
|
statusColor = Colors.orange;
|
||||||
|
} else if (connector.state == MeshCoreConnectionState.disconnecting &&
|
||||||
|
connector.activeTransport == MeshCoreTransportType.tcp) {
|
||||||
|
statusText = l10n.scanner_disconnecting;
|
||||||
|
statusColor = Colors.orange;
|
||||||
|
} else {
|
||||||
|
statusText = l10n.tcpStatus_notConnected;
|
||||||
|
statusColor = Colors.grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||||
|
color: statusColor.withValues(alpha: 0.1),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.circle, size: 12, color: statusColor),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
statusText,
|
||||||
|
style: TextStyle(
|
||||||
|
color: statusColor,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _connectTcp() async {
|
||||||
|
if (_connector.state == MeshCoreConnectionState.connecting ||
|
||||||
|
_connector.state == MeshCoreConnectionState.connected ||
|
||||||
|
_connector.state == MeshCoreConnectionState.disconnecting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final host = _hostController.text.trim();
|
||||||
|
final parsedPort = int.tryParse(_portController.text.trim());
|
||||||
|
if (host.isEmpty) {
|
||||||
|
_showError(context.l10n.tcpErrorHostRequired);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (parsedPort == null || parsedPort < 1 || parsedPort > 65535) {
|
||||||
|
_showError(context.l10n.tcpErrorPortInvalid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _connector.connectTcp(host: host, port: parsedPort);
|
||||||
|
} catch (error) {
|
||||||
|
if (!mounted) return;
|
||||||
|
_showError(_friendlyErrorMessage(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showError(String message) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(message), backgroundColor: Colors.red),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _friendlyErrorMessage(Object error) {
|
||||||
|
if (error is UnsupportedError) {
|
||||||
|
return context.l10n.tcpErrorUnsupported;
|
||||||
|
}
|
||||||
|
if (error is TimeoutException) {
|
||||||
|
return context.l10n.tcpErrorTimedOut;
|
||||||
|
}
|
||||||
|
if (error is StateError) {
|
||||||
|
return context.l10n.tcpConnectionFailed(error.message);
|
||||||
|
}
|
||||||
|
if (error is ArgumentError) {
|
||||||
|
return context.l10n.tcpConnectionFailed(
|
||||||
|
error.message?.toString() ?? error.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context.l10n.tcpConnectionFailed(error.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,30 +10,22 @@ import '../connector/meshcore_connector.dart';
|
|||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
import '../services/app_settings_service.dart';
|
import '../services/app_settings_service.dart';
|
||||||
import '../services/repeater_command_service.dart';
|
import '../services/repeater_command_service.dart';
|
||||||
|
import '../utils/app_logger.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';
|
import '../utils/battery_utils.dart';
|
||||||
|
|
||||||
class TelemetryScreen extends StatefulWidget {
|
class TelemetryScreen extends StatefulWidget {
|
||||||
final Contact repeater;
|
final Contact contact;
|
||||||
final String password;
|
|
||||||
|
|
||||||
const TelemetryScreen({
|
const TelemetryScreen({super.key, required this.contact});
|
||||||
super.key,
|
|
||||||
required this.repeater,
|
|
||||||
required this.password,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<TelemetryScreen> createState() => _TelemetryScreenState();
|
State<TelemetryScreen> createState() => _TelemetryScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TelemetryScreenState extends State<TelemetryScreen> {
|
class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||||
static const int _statusPayloadOffset = 8;
|
int _tagData = 0;
|
||||||
static const int _statusStatsSize = 52;
|
|
||||||
static const int _statusResponseBytes =
|
|
||||||
_statusPayloadOffset + _statusStatsSize;
|
|
||||||
Uint8List _tagData = Uint8List(4);
|
|
||||||
|
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
bool _isLoaded = false;
|
bool _isLoaded = false;
|
||||||
@@ -44,6 +36,26 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||||||
PathSelection? _pendingStatusSelection;
|
PathSelection? _pendingStatusSelection;
|
||||||
List<Map<String, dynamic>>? _parsedTelemetry;
|
List<Map<String, dynamic>>? _parsedTelemetry;
|
||||||
|
|
||||||
|
int _tripTime = 0;
|
||||||
|
|
||||||
|
int _resolveContactIndex = -1;
|
||||||
|
|
||||||
|
Contact _resolveContact(MeshCoreConnector connector) {
|
||||||
|
if (_resolveContactIndex >= 0 &&
|
||||||
|
_resolveContactIndex < connector.contacts.length &&
|
||||||
|
connector.contacts[_resolveContactIndex].publicKeyHex ==
|
||||||
|
widget.contact.publicKeyHex) {
|
||||||
|
return connector.contacts[_resolveContactIndex];
|
||||||
|
}
|
||||||
|
_resolveContactIndex = connector.contacts.indexWhere(
|
||||||
|
(c) => c.publicKeyHex == widget.contact.publicKeyHex,
|
||||||
|
);
|
||||||
|
if (_resolveContactIndex == -1) {
|
||||||
|
return widget.contact;
|
||||||
|
}
|
||||||
|
return connector.contacts[_resolveContactIndex];
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -60,27 +72,62 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||||||
// Listen for incoming text messages from the repeater
|
// Listen for incoming text messages from the repeater
|
||||||
_frameSubscription = connector.receivedFrames.listen((frame) {
|
_frameSubscription = connector.receivedFrames.listen((frame) {
|
||||||
if (frame.isEmpty) return;
|
if (frame.isEmpty) return;
|
||||||
|
final reader = BufferReader(frame);
|
||||||
if (frame[0] == respCodeSent) {
|
try {
|
||||||
_tagData = frame.sublist(2, 6);
|
final cmd = reader.readByte();
|
||||||
|
if (cmd == respCodeSent) {
|
||||||
|
reader.skipBytes(1); // Skip the reserved byte
|
||||||
|
_tagData = reader.readUInt32LE();
|
||||||
|
_tripTime = reader.readUInt32LE();
|
||||||
|
_statusTimeout?.cancel();
|
||||||
|
_statusTimeout = Timer(Duration(milliseconds: _tripTime), () {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_isLoaded = false;
|
||||||
|
});
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(context.l10n.telemetry_requestTimeout),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_recordTelemetryResult(false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's a binary response
|
// Check if it's a binary response
|
||||||
if (frame[0] == pushCodeBinaryResponse &&
|
if (cmd == pushCodeBinaryResponse) {
|
||||||
listEquals(frame.sublist(2, 6), _tagData)) {
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
_handleStatusResponse(frame.sublist(6));
|
reader.skipBytes(1); // Skip the reserved byte
|
||||||
|
if (reader.readUInt32LE() != _tagData) return;
|
||||||
|
_handleTelemetryResponse(reader.readRemainingBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a telemetry response (for chat contacts)
|
||||||
|
if (cmd == pushCodeTelemetryResponse) {
|
||||||
|
reader.skipBytes(1); // Skip the reserved byte
|
||||||
|
final pubkey = reader.readBytes(6);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (!listEquals(widget.contact.publicKey.sublist(0, 6), pubkey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_handleTelemetryResponse(reader.readRemainingBytes());
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
appLogger.error('Error parsing incoming frame: $e');
|
||||||
|
// If parsing fails, ignore the frame
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleStatusResponse(Uint8List frame) {
|
void _handleTelemetryResponse(Uint8List frame) {
|
||||||
final parsedTelemetry = CayenneLpp.parseByChannel(frame);
|
final parsedTelemetry = CayenneLpp.parseByChannel(frame);
|
||||||
final batteryMv = _extractTelemetryBatteryMillivolts(parsedTelemetry);
|
final batteryMv = _extractTelemetryBatteryMillivolts(parsedTelemetry);
|
||||||
if (batteryMv != null) {
|
if (batteryMv != null) {
|
||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
connector.updateRepeaterBatterySnapshot(
|
connector.updateRepeaterBatterySnapshot(
|
||||||
widget.repeater.publicKeyHex,
|
widget.contact.publicKeyHex,
|
||||||
batteryMv,
|
batteryMv,
|
||||||
source: 'telemetry',
|
source: 'telemetry',
|
||||||
);
|
);
|
||||||
@@ -105,13 +152,6 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Contact _resolveRepeater(MeshCoreConnector connector) {
|
|
||||||
return connector.contacts.firstWhere(
|
|
||||||
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
|
|
||||||
orElse: () => widget.repeater,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadTelemetry() async {
|
Future<void> _loadTelemetry() async {
|
||||||
if (_commandService == null) return;
|
if (_commandService == null) return;
|
||||||
|
|
||||||
@@ -121,41 +161,20 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
final repeater = _resolveRepeater(connector);
|
final selection = await connector.preparePathForContactSend(
|
||||||
final selection = await connector.preparePathForContactSend(repeater);
|
_resolveContact(connector),
|
||||||
|
);
|
||||||
_pendingStatusSelection = selection;
|
_pendingStatusSelection = selection;
|
||||||
final frame = buildSendBinaryReq(
|
Uint8List frame;
|
||||||
repeater.publicKey,
|
if (widget.contact.type != advTypeChat) {
|
||||||
|
frame = buildSendBinaryReq(
|
||||||
|
widget.contact.publicKey,
|
||||||
payload: Uint8List.fromList([reqTypeGetTelemetry]),
|
payload: Uint8List.fromList([reqTypeGetTelemetry]),
|
||||||
);
|
);
|
||||||
await connector.sendFrame(frame);
|
} else {
|
||||||
|
frame = buildSendTelemetryReq(widget.contact.publicKey);
|
||||||
final pathLengthValue = selection.useFlood ? -1 : selection.hopCount;
|
|
||||||
var messageBytes = frame.length >= _statusResponseBytes
|
|
||||||
? frame.length
|
|
||||||
: _statusResponseBytes;
|
|
||||||
if (messageBytes < maxFrameSize) {
|
|
||||||
messageBytes = maxFrameSize;
|
|
||||||
}
|
}
|
||||||
final timeoutMs = connector.calculateTimeout(
|
await connector.sendFrame(frame);
|
||||||
pathLength: pathLengthValue,
|
|
||||||
messageBytes: messageBytes,
|
|
||||||
);
|
|
||||||
_statusTimeout?.cancel();
|
|
||||||
_statusTimeout = Timer(Duration(milliseconds: timeoutMs), () {
|
|
||||||
if (!mounted) return;
|
|
||||||
setState(() {
|
|
||||||
_isLoading = false;
|
|
||||||
_isLoaded = false;
|
|
||||||
});
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(context.l10n.telemetry_requestTimeout),
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
_recordStatusResult(false);
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -173,12 +192,16 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _recordStatusResult(bool success) {
|
void _recordTelemetryResult(bool success) {
|
||||||
final selection = _pendingStatusSelection;
|
final selection = _pendingStatusSelection;
|
||||||
if (selection == null) return;
|
if (selection == null) return;
|
||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
final repeater = _resolveRepeater(connector);
|
connector.recordRepeaterPathResult(
|
||||||
connector.recordRepeaterPathResult(repeater, selection, success, null);
|
widget.contact,
|
||||||
|
selection,
|
||||||
|
success,
|
||||||
|
null,
|
||||||
|
);
|
||||||
_pendingStatusSelection = null;
|
_pendingStatusSelection = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,8 +219,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||||||
final connector = context.watch<MeshCoreConnector>();
|
final connector = context.watch<MeshCoreConnector>();
|
||||||
final settings = context.watch<AppSettingsService>().settings;
|
final settings = context.watch<AppSettingsService>().settings;
|
||||||
final isImperialUnits = settings.unitSystem == UnitSystem.imperial;
|
final isImperialUnits = settings.unitSystem == UnitSystem.imperial;
|
||||||
final repeater = _resolveRepeater(connector);
|
final isFloodMode = widget.contact.pathOverride == -1;
|
||||||
final isFloodMode = repeater.pathOverride == -1;
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -210,7 +232,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
repeater.name,
|
widget.contact.name,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.normal,
|
fontWeight: FontWeight.normal,
|
||||||
@@ -225,9 +247,9 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||||||
tooltip: l10n.repeater_routingMode,
|
tooltip: l10n.repeater_routingMode,
|
||||||
onSelected: (mode) async {
|
onSelected: (mode) async {
|
||||||
if (mode == 'flood') {
|
if (mode == 'flood') {
|
||||||
await connector.setPathOverride(repeater, pathLen: -1);
|
await connector.setPathOverride(widget.contact, pathLen: -1);
|
||||||
} else {
|
} else {
|
||||||
await connector.setPathOverride(repeater, pathLen: null);
|
await connector.setPathOverride(widget.contact, pathLen: null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
@@ -283,7 +305,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||||||
icon: const Icon(Icons.timeline),
|
icon: const Icon(Icons.timeline),
|
||||||
tooltip: l10n.repeater_pathManagement,
|
tooltip: l10n.repeater_pathManagement,
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
PathManagementDialog.show(context, contact: repeater),
|
PathManagementDialog.show(context, contact: widget.contact),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: _isLoading
|
icon: _isLoading
|
||||||
@@ -437,7 +459,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
final connector = context.watch<MeshCoreConnector>();
|
final connector = context.watch<MeshCoreConnector>();
|
||||||
final batteryMv =
|
final batteryMv =
|
||||||
connector.getRepeaterBatteryMillivolts(widget.repeater.publicKeyHex) ??
|
connector.getRepeaterBatteryMillivolts(widget.contact.publicKeyHex) ??
|
||||||
(telemetryVolts == null ? null : (telemetryVolts * 1000).round());
|
(telemetryVolts == null ? null : (telemetryVolts * 1000).round());
|
||||||
if (batteryMv == null) return l10n.common_notAvailable;
|
if (batteryMv == null) return l10n.common_notAvailable;
|
||||||
final chemistry = _batteryChemistry();
|
final chemistry = _batteryChemistry();
|
||||||
@@ -449,7 +471,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
|||||||
String _batteryChemistry() {
|
String _batteryChemistry() {
|
||||||
final settingsService = context.read<AppSettingsService>();
|
final settingsService = context.read<AppSettingsService>();
|
||||||
return settingsService.batteryChemistryForRepeater(
|
return settingsService.batteryChemistryForRepeater(
|
||||||
widget.repeater.publicKeyHex,
|
widget.contact.publicKeyHex,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,441 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../connector/meshcore_connector.dart';
|
||||||
|
import '../l10n/l10n.dart';
|
||||||
|
import '../utils/app_logger.dart';
|
||||||
|
import '../utils/platform_info.dart';
|
||||||
|
import '../utils/usb_port_labels.dart';
|
||||||
|
import '../widgets/adaptive_app_bar_title.dart';
|
||||||
|
import 'contacts_screen.dart';
|
||||||
|
import 'scanner_screen.dart';
|
||||||
|
import 'tcp_screen.dart';
|
||||||
|
|
||||||
|
class UsbScreen extends StatefulWidget {
|
||||||
|
const UsbScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<UsbScreen> createState() => _UsbScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _UsbScreenState extends State<UsbScreen> {
|
||||||
|
final List<String> _ports = <String>[];
|
||||||
|
bool _isLoadingPorts = true;
|
||||||
|
bool _navigatedToContacts = false;
|
||||||
|
bool _didScheduleInitialLoad = false;
|
||||||
|
Timer? _hotPlugTimer;
|
||||||
|
late final MeshCoreConnector _connector;
|
||||||
|
late final VoidCallback _connectionListener;
|
||||||
|
|
||||||
|
bool get _supportsHotPlug =>
|
||||||
|
PlatformInfo.isWindows || PlatformInfo.isLinux || PlatformInfo.isMacOS;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_connector = context.read<MeshCoreConnector>();
|
||||||
|
_connectionListener = () {
|
||||||
|
if (!mounted) return;
|
||||||
|
if (_connector.state == MeshCoreConnectionState.disconnected) {
|
||||||
|
_navigatedToContacts = false;
|
||||||
|
}
|
||||||
|
if (_connector.state == MeshCoreConnectionState.connected &&
|
||||||
|
_connector.isUsbTransportConnected &&
|
||||||
|
!_navigatedToContacts) {
|
||||||
|
_navigatedToContacts = true;
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(builder: (_) => const ContactsScreen()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
_connector.addListener(_connectionListener);
|
||||||
|
_startHotPlugTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
_connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus);
|
||||||
|
_connector.setUsbFallbackDeviceName(context.l10n.usbFallbackDeviceName);
|
||||||
|
if (!_didScheduleInitialLoad) {
|
||||||
|
_didScheduleInitialLoad = true;
|
||||||
|
unawaited(_loadPorts());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_hotPlugTimer?.cancel();
|
||||||
|
_hotPlugTimer = null;
|
||||||
|
_connector.removeListener(_connectionListener);
|
||||||
|
if (!_navigatedToContacts &&
|
||||||
|
_connector.activeTransport == MeshCoreTransportType.usb &&
|
||||||
|
_connector.state != MeshCoreConnectionState.disconnected) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
unawaited(_connector.disconnect(manual: true));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => Navigator.of(context).maybePop(),
|
||||||
|
),
|
||||||
|
title: AdaptiveAppBarTitle(context.l10n.usbScreenTitle),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: Consumer<MeshCoreConnector>(
|
||||||
|
builder: (context, connector, child) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_buildStatusBar(context, connector),
|
||||||
|
Expanded(child: _buildPortList(context, connector)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bottomNavigationBar: Consumer<MeshCoreConnector>(
|
||||||
|
builder: (context, connector, child) {
|
||||||
|
final isLoading = _isLoadingPorts;
|
||||||
|
final showBle = true;
|
||||||
|
final showTcp = !PlatformInfo.isWeb;
|
||||||
|
|
||||||
|
return SafeArea(
|
||||||
|
top: false,
|
||||||
|
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
if (showTcp)
|
||||||
|
FloatingActionButton.extended(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(builder: (_) => const TcpScreen()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
heroTag: 'usb_tcp_action',
|
||||||
|
extendedPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.lan),
|
||||||
|
label: Text(context.l10n.connectionChoiceTcpLabel),
|
||||||
|
),
|
||||||
|
if (showTcp && showBle) const SizedBox(width: 12),
|
||||||
|
if (showBle)
|
||||||
|
FloatingActionButton.extended(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => const ScannerScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
heroTag: 'usb_ble_action',
|
||||||
|
extendedPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.bluetooth),
|
||||||
|
label: Text(context.l10n.connectionChoiceBluetoothLabel),
|
||||||
|
),
|
||||||
|
if ((showTcp || showBle) && !_supportsHotPlug)
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
if (!_supportsHotPlug)
|
||||||
|
FloatingActionButton.extended(
|
||||||
|
onPressed: isLoading ? null : _loadPorts,
|
||||||
|
heroTag: 'usb_refresh_action',
|
||||||
|
extendedPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
),
|
||||||
|
icon: isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.usb),
|
||||||
|
label: Text(context.l10n.scanner_scan),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatusBar(BuildContext context, MeshCoreConnector connector) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
String statusText;
|
||||||
|
Color statusColor;
|
||||||
|
|
||||||
|
if (_isLoadingPorts) {
|
||||||
|
statusText = l10n.usbStatus_searching;
|
||||||
|
statusColor = Colors.blue;
|
||||||
|
} else if (connector.isUsbTransportConnected) {
|
||||||
|
switch (connector.state) {
|
||||||
|
case MeshCoreConnectionState.connected:
|
||||||
|
statusText = l10n.scanner_connectedTo(
|
||||||
|
connector.activeUsbPortDisplayLabel ?? 'USB',
|
||||||
|
);
|
||||||
|
statusColor = Colors.green;
|
||||||
|
case MeshCoreConnectionState.disconnecting:
|
||||||
|
statusText = l10n.scanner_disconnecting;
|
||||||
|
statusColor = Colors.orange;
|
||||||
|
default:
|
||||||
|
statusText = l10n.usbStatus_notConnected;
|
||||||
|
statusColor = Colors.grey;
|
||||||
|
}
|
||||||
|
} else if (connector.state == MeshCoreConnectionState.connecting &&
|
||||||
|
connector.activeTransport == MeshCoreTransportType.usb) {
|
||||||
|
statusText = l10n.usbStatus_connecting;
|
||||||
|
statusColor = Colors.orange;
|
||||||
|
} else {
|
||||||
|
statusText = l10n.usbStatus_notConnected;
|
||||||
|
statusColor = Colors.grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||||
|
color: statusColor.withValues(alpha: 0.1),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.circle, size: 12, color: statusColor),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
statusText,
|
||||||
|
style: TextStyle(
|
||||||
|
color: statusColor,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPortList(BuildContext context, MeshCoreConnector connector) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
|
||||||
|
if (_isLoadingPorts) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.usb, size: 64, color: Colors.grey[400]),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
l10n.usbStatus_searching,
|
||||||
|
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_ports.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.usb, size: 64, color: Colors.grey[400]),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
l10n.usbScreenEmptyState,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final isConnecting =
|
||||||
|
connector.state == MeshCoreConnectionState.connecting &&
|
||||||
|
connector.activeTransport == MeshCoreTransportType.usb;
|
||||||
|
|
||||||
|
return ListView.separated(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
itemCount: _ports.length,
|
||||||
|
separatorBuilder: (context, index) => const Divider(),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final port = _ports[index];
|
||||||
|
final displayName = friendlyUsbPortName(port);
|
||||||
|
final rawName = normalizeUsbPortName(port);
|
||||||
|
final showRawName =
|
||||||
|
rawName != displayName && !rawName.startsWith('web:');
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.usb),
|
||||||
|
title: Text(
|
||||||
|
displayName,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
subtitle: showRawName ? Text(rawName) : null,
|
||||||
|
trailing: ElevatedButton(
|
||||||
|
onPressed: isConnecting ? null : () => _connectPort(port),
|
||||||
|
child: Text(l10n.common_connect),
|
||||||
|
),
|
||||||
|
onTap: isConnecting ? null : () => _connectPort(port),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startHotPlugTimer() {
|
||||||
|
if (!_supportsHotPlug) return;
|
||||||
|
_hotPlugTimer?.cancel();
|
||||||
|
_hotPlugTimer = Timer.periodic(const Duration(seconds: 2), (_) {
|
||||||
|
_pollHotPlug();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pollHotPlug() async {
|
||||||
|
if (_isLoadingPorts) return;
|
||||||
|
if (!mounted) return;
|
||||||
|
// Don't poll while connecting or connected.
|
||||||
|
if (_connector.state != MeshCoreConnectionState.disconnected) return;
|
||||||
|
try {
|
||||||
|
final ports = await _connector.listUsbPorts();
|
||||||
|
if (!mounted) return;
|
||||||
|
final added = ports.where((p) => !_ports.contains(p)).toList();
|
||||||
|
final removed = _ports.where((p) => !ports.contains(p)).toList();
|
||||||
|
if (added.isEmpty && removed.isEmpty) return;
|
||||||
|
setState(() {
|
||||||
|
_ports
|
||||||
|
..clear()
|
||||||
|
..addAll(ports);
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
// Silent — hot-plug failures are non-critical.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadPorts() async {
|
||||||
|
if (!mounted) return;
|
||||||
|
_connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoadingPorts = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final ports = await _connector.listUsbPorts();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_ports
|
||||||
|
..clear()
|
||||||
|
..addAll(ports);
|
||||||
|
_isLoadingPorts = false;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_ports.clear();
|
||||||
|
_isLoadingPorts = false;
|
||||||
|
});
|
||||||
|
_showError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _connectPort(String port) async {
|
||||||
|
if (_connector.state != MeshCoreConnectionState.disconnected) return;
|
||||||
|
|
||||||
|
final rawPortName = normalizeUsbPortName(port);
|
||||||
|
appLogger.info(
|
||||||
|
'Connect tapped for $port (raw: $rawPortName)',
|
||||||
|
tag: 'UsbScreen',
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _connector.connectUsb(portName: rawPortName);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
appLogger.error(
|
||||||
|
'Connect failed for $rawPortName: $error\n$stackTrace',
|
||||||
|
tag: 'UsbScreen',
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
_showError(error);
|
||||||
|
unawaited(_loadPorts());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showError(Object error) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(_friendlyErrorMessage(error)),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _friendlyErrorMessage(Object error) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
|
||||||
|
if (error is PlatformException) {
|
||||||
|
switch (error.code) {
|
||||||
|
case 'usb_permission_denied':
|
||||||
|
return l10n.usbErrorPermissionDenied;
|
||||||
|
case 'usb_device_missing':
|
||||||
|
case 'usb_device_detached':
|
||||||
|
return l10n.usbErrorDeviceMissing;
|
||||||
|
case 'usb_invalid_port':
|
||||||
|
return l10n.usbErrorInvalidPort;
|
||||||
|
case 'usb_busy':
|
||||||
|
return l10n.usbErrorBusy;
|
||||||
|
case 'usb_not_connected':
|
||||||
|
return l10n.usbErrorNotConnected;
|
||||||
|
case 'usb_open_failed':
|
||||||
|
case 'usb_driver_missing':
|
||||||
|
return l10n.usbErrorOpenFailed;
|
||||||
|
case 'usb_connect_failed':
|
||||||
|
return l10n.usbErrorConnectFailed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error is UnsupportedError) {
|
||||||
|
return l10n.usbErrorUnsupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error is StateError) {
|
||||||
|
final msg = error.message;
|
||||||
|
if (msg.contains('already active')) return l10n.usbErrorAlreadyActive;
|
||||||
|
if (msg.contains('No USB serial device selected')) {
|
||||||
|
return l10n.usbErrorNoDeviceSelected;
|
||||||
|
}
|
||||||
|
if (msg.contains('not open') || msg.contains('closed')) {
|
||||||
|
return l10n.usbErrorPortClosed;
|
||||||
|
}
|
||||||
|
if (msg.contains('Timed out')) return l10n.usbErrorConnectTimedOut;
|
||||||
|
if (msg.contains('Failed to open')) return l10n.usbErrorOpenFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error is TimeoutException) {
|
||||||
|
return l10n.usbErrorConnectTimedOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
return error.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,8 +51,14 @@ class AppDebugLogService extends ChangeNotifier {
|
|||||||
String message, {
|
String message, {
|
||||||
String tag = 'App',
|
String tag = 'App',
|
||||||
AppDebugLogLevel level = AppDebugLogLevel.info,
|
AppDebugLogLevel level = AppDebugLogLevel.info,
|
||||||
|
bool noNotify = false,
|
||||||
}) {
|
}) {
|
||||||
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(
|
||||||
@@ -67,22 +73,24 @@ class AppDebugLogService extends ChangeNotifier {
|
|||||||
_entries.removeRange(0, _entries.length - maxEntries);
|
_entries.removeRange(0, _entries.length - maxEntries);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!noNotify) {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
// Also print to console for development
|
// Also print to console for development
|
||||||
debugPrint('[$tag] $message');
|
debugPrint('[$tag] $message');
|
||||||
}
|
}
|
||||||
|
|
||||||
void info(String message, {String tag = 'App'}) {
|
void info(String message, {String tag = 'App', bool noNotify = false}) {
|
||||||
log(message, tag: tag, level: AppDebugLogLevel.info);
|
log(message, tag: tag, level: AppDebugLogLevel.info, noNotify: noNotify);
|
||||||
}
|
}
|
||||||
|
|
||||||
void warn(String message, {String tag = 'App'}) {
|
void warn(String message, {String tag = 'App', bool noNotify = false}) {
|
||||||
log(message, tag: tag, level: AppDebugLogLevel.warning);
|
log(message, tag: tag, level: AppDebugLogLevel.warning, noNotify: noNotify);
|
||||||
}
|
}
|
||||||
|
|
||||||
void error(String message, {String tag = 'App'}) {
|
void error(String message, {String tag = 'App', bool noNotify = false}) {
|
||||||
log(message, tag: tag, level: AppDebugLogLevel.error);
|
log(message, tag: tag, level: AppDebugLogLevel.error, noNotify: noNotify);
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ class AppSettingsService extends ChangeNotifier {
|
|||||||
await updateSettings(_settings.copyWith(mapShowOtherNodes: value));
|
await updateSettings(_settings.copyWith(mapShowOtherNodes: value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setMapShowOverlaps(bool value) async {
|
||||||
|
await updateSettings(_settings.copyWith(mapShowOverlaps: value));
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> setMapTimeFilterHours(double value) async {
|
Future<void> setMapTimeFilterHours(double value) async {
|
||||||
await updateSettings(_settings.copyWith(mapTimeFilterHours: value));
|
await updateSettings(_settings.copyWith(mapTimeFilterHours: value));
|
||||||
}
|
}
|
||||||
@@ -80,6 +84,10 @@ 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 {
|
Future<void> setEnableMessageTracing(bool value) async {
|
||||||
await updateSettings(_settings.copyWith(enableMessageTracing: value));
|
await updateSettings(_settings.copyWith(enableMessageTracing: value));
|
||||||
}
|
}
|
||||||
@@ -116,6 +124,30 @@ class AppSettingsService extends ChangeNotifier {
|
|||||||
await updateSettings(_settings.copyWith(autoRouteRotationEnabled: value));
|
await updateSettings(_settings.copyWith(autoRouteRotationEnabled: value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setMaxRouteWeight(double value) async {
|
||||||
|
await updateSettings(_settings.copyWith(maxRouteWeight: value));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setInitialRouteWeight(double value) async {
|
||||||
|
await updateSettings(_settings.copyWith(initialRouteWeight: value));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setRouteWeightSuccessIncrement(double value) async {
|
||||||
|
await updateSettings(
|
||||||
|
_settings.copyWith(routeWeightSuccessIncrement: value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setRouteWeightFailureDecrement(double value) async {
|
||||||
|
await updateSettings(
|
||||||
|
_settings.copyWith(routeWeightFailureDecrement: value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setMaxMessageRetries(int value) async {
|
||||||
|
await updateSettings(_settings.copyWith(maxMessageRetries: value));
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> setThemeMode(String value) async {
|
Future<void> setThemeMode(String value) async {
|
||||||
await updateSettings(_settings.copyWith(themeMode: value));
|
await updateSettings(_settings.copyWith(themeMode: value));
|
||||||
}
|
}
|
||||||
@@ -130,6 +162,10 @@ class AppSettingsService extends ChangeNotifier {
|
|||||||
appLogger.setEnabled(value);
|
appLogger.setEnabled(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setMapShowDiscoveryContacts(bool value) async {
|
||||||
|
await updateSettings(_settings.copyWith(mapShowDiscoveryContacts: value));
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> setBatteryChemistryForDevice(
|
Future<void> setBatteryChemistryForDevice(
|
||||||
String deviceId,
|
String deviceId,
|
||||||
String chemistry,
|
String chemistry,
|
||||||
@@ -174,4 +210,12 @@ class AppSettingsService extends ChangeNotifier {
|
|||||||
..remove(channelName);
|
..remove(channelName);
|
||||||
await updateSettings(_settings.copyWith(mutedChannels: updated));
|
await updateSettings(_settings.copyWith(mutedChannels: updated));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setTcpServerAddress(String value) async {
|
||||||
|
await updateSettings(_settings.copyWith(tcpServerAddress: value));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setTcpServerPort(int value) async {
|
||||||
|
await updateSettings(_settings.copyWith(tcpServerPort: value));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,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();
|
||||||
|
|||||||
@@ -172,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:
|
||||||
@@ -215,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:
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class ChatTextScaleService extends ChangeNotifier {
|
|||||||
|
|
||||||
void _commitScale() {
|
void _commitScale() {
|
||||||
_saveTimer?.cancel();
|
_saveTimer?.cancel();
|
||||||
PrefsManager.instance.setDouble(_prefKey, _scale);
|
unawaited(PrefsManager.instance.setDouble(_prefKey, _scale));
|
||||||
}
|
}
|
||||||
|
|
||||||
double _clamp(double value) => value.clamp(_minScale, _maxScale).toDouble();
|
double _clamp(double value) => value.clamp(_minScale, _maxScale).toDouble();
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import 'app_debug_log_service.dart';
|
|||||||
|
|
||||||
class _AckHistoryEntry {
|
class _AckHistoryEntry {
|
||||||
final String messageId;
|
final String messageId;
|
||||||
final List<Uint8List> ackHashes;
|
final List<int> ackHashes;
|
||||||
final DateTime timestamp;
|
final DateTime timestamp;
|
||||||
|
|
||||||
_AckHistoryEntry({
|
_AckHistoryEntry({
|
||||||
@@ -21,75 +21,84 @@ class _AckHistoryEntry {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AckHashMapping {
|
/// (messageId, timestamp, attemptIndex) — stored per ACK hash for O(1) lookup.
|
||||||
final String messageId;
|
typedef AckHashMapping = ({
|
||||||
final DateTime timestamp;
|
String messageId,
|
||||||
|
DateTime timestamp,
|
||||||
|
int attemptIndex,
|
||||||
|
});
|
||||||
|
|
||||||
_AckHashMapping({required this.messageId, required this.timestamp});
|
class RetryServiceConfig {
|
||||||
|
final void Function(Contact, String, int, int) sendMessage;
|
||||||
|
final void Function(String, Message) addMessage;
|
||||||
|
final void Function(Message) updateMessage;
|
||||||
|
final Function(Contact)? clearContactPath;
|
||||||
|
final Function(Contact, Uint8List, int)? setContactPath;
|
||||||
|
final int Function(int pathLength, int messageBytes, {String? contactKey})?
|
||||||
|
calculateTimeout;
|
||||||
|
final Uint8List? Function()? getSelfPublicKey;
|
||||||
|
final String Function(Contact, String)? prepareContactOutboundText;
|
||||||
|
final AppSettingsService? appSettingsService;
|
||||||
|
final AppDebugLogService? debugLogService;
|
||||||
|
final void Function(String, PathSelection, bool, int?)? recordPathResult;
|
||||||
|
final void Function(String, int, int, int)? onDeliveryObserved;
|
||||||
|
final PathSelection? Function(
|
||||||
|
String contactKey,
|
||||||
|
int attemptIndex,
|
||||||
|
int maxRetries,
|
||||||
|
List<PathSelection> recentSelections,
|
||||||
|
)?
|
||||||
|
selectRetryPath;
|
||||||
|
|
||||||
|
const RetryServiceConfig({
|
||||||
|
required this.sendMessage,
|
||||||
|
required this.addMessage,
|
||||||
|
required this.updateMessage,
|
||||||
|
this.clearContactPath,
|
||||||
|
this.setContactPath,
|
||||||
|
this.calculateTimeout,
|
||||||
|
this.getSelfPublicKey,
|
||||||
|
this.prepareContactOutboundText,
|
||||||
|
this.appSettingsService,
|
||||||
|
this.debugLogService,
|
||||||
|
this.recordPathResult,
|
||||||
|
this.onDeliveryObserved,
|
||||||
|
this.selectRetryPath,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class MessageRetryService extends ChangeNotifier {
|
class MessageRetryService extends ChangeNotifier {
|
||||||
static const int maxRetries = 5;
|
|
||||||
static const int maxAckHistorySize = 100;
|
static const int maxAckHistorySize = 100;
|
||||||
|
int _maxRetries = 5;
|
||||||
|
int get maxRetries => _maxRetries;
|
||||||
|
|
||||||
final Map<String, Timer> _timeoutTimers = {};
|
final Map<String, Timer> _timeoutTimers = {};
|
||||||
final Map<String, Message> _pendingMessages = {};
|
final Map<String, Message> _pendingMessages = {};
|
||||||
final Map<String, Contact> _pendingContacts = {};
|
final Map<String, Contact> _pendingContacts = {};
|
||||||
final Map<String, PathSelection> _pendingPathSelections = {};
|
final Map<String, List<PathSelection>> _attemptPathHistory = {};
|
||||||
final Map<String, _AckHashMapping> _ackHashToMessageId =
|
final Map<String, AckHashMapping> _ackHashToMessageId = {};
|
||||||
{}; // ackHashHex → messageId + timestamp for O(1) lookup
|
final Map<String, List<int>> _expectedAckHashes = {};
|
||||||
final Map<String, List<Uint8List>> _expectedAckHashes =
|
final List<_AckHistoryEntry> _ackHistory = [];
|
||||||
{}; // Track all expected ACKs for retries (for history)
|
final Map<String, List<String>> _sendQueue = {};
|
||||||
final List<_AckHistoryEntry> _ackHistory =
|
final Set<String> _activeMessages = {};
|
||||||
[]; // Rolling buffer of recent ACK hashes
|
final Set<String> _resolvedMessages = {};
|
||||||
final Map<String, List<String>> _pendingMessageQueuePerContact =
|
final Map<String, String> _expectedHashToMessageId = {};
|
||||||
{}; // contactPubKeyHex → FIFO queue of messageIds (DEPRECATED - will be removed)
|
|
||||||
final Map<String, String> _expectedHashToMessageId =
|
|
||||||
{}; // expectedAckHashHex → messageId (for matching RESP_CODE_SENT by hash)
|
|
||||||
|
|
||||||
Function(Contact, String, int, int)? _sendMessageCallback;
|
RetryServiceConfig? _config;
|
||||||
Function(String, Message)? _addMessageCallback;
|
|
||||||
Function(Message)? _updateMessageCallback;
|
|
||||||
Function(Contact)? _clearContactPathCallback;
|
|
||||||
Function(Contact, Uint8List, int)? _setContactPathCallback;
|
|
||||||
Function(int, int)? _calculateTimeoutCallback;
|
|
||||||
Uint8List? Function()? _getSelfPublicKeyCallback;
|
|
||||||
String Function(Contact, String)? _prepareContactOutboundTextCallback;
|
|
||||||
AppSettingsService? _appSettingsService;
|
|
||||||
AppDebugLogService? _debugLogService;
|
|
||||||
Function(String, PathSelection, bool, int?)? _recordPathResultCallback;
|
|
||||||
|
|
||||||
MessageRetryService();
|
MessageRetryService();
|
||||||
|
|
||||||
void initialize({
|
void initialize(RetryServiceConfig config) {
|
||||||
required Function(Contact, String, int, int) sendMessageCallback,
|
_config = config;
|
||||||
required Function(String, Message) addMessageCallback,
|
}
|
||||||
required Function(Message) updateMessageCallback,
|
|
||||||
Function(Contact)? clearContactPathCallback,
|
void setMaxRetries(int value) {
|
||||||
Function(Contact, Uint8List, int)? setContactPathCallback,
|
_maxRetries = value.clamp(2, 10);
|
||||||
Function(int pathLength, int messageBytes)? calculateTimeoutCallback,
|
|
||||||
Uint8List? Function()? getSelfPublicKeyCallback,
|
|
||||||
String Function(Contact, String)? prepareContactOutboundTextCallback,
|
|
||||||
AppSettingsService? appSettingsService,
|
|
||||||
AppDebugLogService? debugLogService,
|
|
||||||
Function(String, PathSelection, bool, int?)? recordPathResultCallback,
|
|
||||||
}) {
|
|
||||||
_sendMessageCallback = sendMessageCallback;
|
|
||||||
_addMessageCallback = addMessageCallback;
|
|
||||||
_updateMessageCallback = updateMessageCallback;
|
|
||||||
_clearContactPathCallback = clearContactPathCallback;
|
|
||||||
_setContactPathCallback = setContactPathCallback;
|
|
||||||
_calculateTimeoutCallback = calculateTimeoutCallback;
|
|
||||||
_getSelfPublicKeyCallback = getSelfPublicKeyCallback;
|
|
||||||
_prepareContactOutboundTextCallback = prepareContactOutboundTextCallback;
|
|
||||||
_appSettingsService = appSettingsService;
|
|
||||||
_debugLogService = debugLogService;
|
|
||||||
_recordPathResultCallback = recordPathResultCallback;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute expected ACK hash using same algorithm as firmware:
|
/// Compute expected ACK hash using same algorithm as firmware:
|
||||||
/// SHA256([timestamp(4)][attempt(1)][text][sender_pubkey(32)]) -> first 4 bytes
|
/// SHA256([timestamp(4)][attempt(1)][text][sender_pubkey(32)]) -> first 4 bytes
|
||||||
static Uint8List computeExpectedAckHash(
|
static int computeExpectedAckHash(
|
||||||
int timestampSeconds,
|
int timestampSeconds,
|
||||||
int attempt,
|
int attempt,
|
||||||
String text,
|
String text,
|
||||||
@@ -117,23 +126,22 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
|
|
||||||
// Compute SHA256 and return first 4 bytes
|
// Compute SHA256 and return first 4 bytes
|
||||||
final hash = sha256.convert(buffer);
|
final hash = sha256.convert(buffer);
|
||||||
return Uint8List.fromList(hash.bytes.sublist(0, 4));
|
final bytes = Uint8List.fromList(hash.bytes.sublist(0, 4));
|
||||||
|
return (bytes[3] << 24) | (bytes[2] << 16) | (bytes[1] << 8) | bytes[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> sendMessageWithRetry({
|
Future<void> sendMessageWithRetry({
|
||||||
required Contact contact,
|
required Contact contact,
|
||||||
required String text,
|
required String text,
|
||||||
PathSelection? pathSelection,
|
|
||||||
Uint8List? pathBytes,
|
Uint8List? pathBytes,
|
||||||
int? pathLength,
|
int? pathLength,
|
||||||
}) async {
|
}) async {
|
||||||
final messageId = const Uuid().v4();
|
final messageId = const Uuid().v4();
|
||||||
final useFlood = pathSelection?.useFlood ?? false;
|
final resolved = resolvePathSelection(contact);
|
||||||
final messagePathBytes =
|
final messagePathBytes =
|
||||||
pathBytes ?? _resolveMessagePathBytes(contact, useFlood, pathSelection);
|
pathBytes ?? Uint8List.fromList(resolved.pathBytes);
|
||||||
final messagePathLength =
|
final messagePathLength =
|
||||||
pathLength ??
|
pathLength ?? (resolved.useFlood ? -1 : resolved.hopCount);
|
||||||
_resolveMessagePathLength(contact, useFlood, pathSelection);
|
|
||||||
final message = Message(
|
final message = Message(
|
||||||
senderKey: contact.publicKey,
|
senderKey: contact.publicKey,
|
||||||
text: text,
|
text: text,
|
||||||
@@ -148,59 +156,168 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
|
|
||||||
_pendingMessages[messageId] = message;
|
_pendingMessages[messageId] = message;
|
||||||
_pendingContacts[messageId] = contact;
|
_pendingContacts[messageId] = contact;
|
||||||
if (pathSelection != null) {
|
|
||||||
_pendingPathSelections[messageId] = pathSelection;
|
_config?.addMessage(contact.publicKeyHex, message);
|
||||||
|
|
||||||
|
// Queue per contact — only one message in-flight at a time to avoid
|
||||||
|
// overflowing the firmware's 8-entry expected_ack_table.
|
||||||
|
final contactKey = contact.publicKeyHex;
|
||||||
|
_sendQueue[contactKey] ??= [];
|
||||||
|
_sendQueue[contactKey]!.add(messageId);
|
||||||
|
|
||||||
|
if (!_activeMessages.any(
|
||||||
|
(id) => _pendingContacts[id]?.publicKeyHex == contactKey,
|
||||||
|
)) {
|
||||||
|
_sendNextForContact(contactKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_addMessageCallback != null) {
|
void _sendNextForContact(String contactKey) {
|
||||||
_addMessageCallback!(contact.publicKeyHex, message);
|
final queue = _sendQueue[contactKey];
|
||||||
|
if (queue == null) return;
|
||||||
|
|
||||||
|
// Drain stale entries iteratively instead of recursing.
|
||||||
|
while (queue.isNotEmpty) {
|
||||||
|
final messageId = queue.removeAt(0);
|
||||||
|
if (_pendingMessages.containsKey(messageId)) {
|
||||||
|
_activeMessages.add(messageId);
|
||||||
|
_attemptSend(messageId).catchError((e) {
|
||||||
|
debugPrint('_attemptSend threw for $messageId: $e');
|
||||||
|
final msg = _pendingMessages[messageId];
|
||||||
|
if (msg != null) {
|
||||||
|
final failed = msg.copyWith(status: MessageStatus.failed);
|
||||||
|
_pendingMessages[messageId] = failed;
|
||||||
|
_config?.updateMessage(failed);
|
||||||
|
}
|
||||||
|
_onMessageResolved(messageId, contactKey);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await _attemptSend(messageId);
|
void _onMessageResolved(String messageId, String contactKey) {
|
||||||
|
if (_resolvedMessages.contains(messageId)) return;
|
||||||
|
_resolvedMessages.add(messageId);
|
||||||
|
_activeMessages.remove(messageId);
|
||||||
|
_sendNextForContact(contactKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
PathSelection? _selectPathForAttempt(Message message, Contact contact) {
|
||||||
|
final config = _config;
|
||||||
|
if (config == null) return null;
|
||||||
|
final autoRotationEnabled =
|
||||||
|
config.appSettingsService?.settings.autoRouteRotationEnabled == true;
|
||||||
|
if (!autoRotationEnabled ||
|
||||||
|
contact.pathOverride != null ||
|
||||||
|
config.selectRetryPath == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final recentSelections = List<PathSelection>.from(
|
||||||
|
_attemptPathHistory[message.messageId] ?? const <PathSelection>[],
|
||||||
|
);
|
||||||
|
return config.selectRetryPath!(
|
||||||
|
contact.publicKeyHex,
|
||||||
|
message.retryCount,
|
||||||
|
maxRetries,
|
||||||
|
recentSelections,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _recordAttemptPathHistory(String messageId, PathSelection selection) {
|
||||||
|
if (selection.useFlood) return;
|
||||||
|
final history = _attemptPathHistory.putIfAbsent(messageId, () => []);
|
||||||
|
history.add(selection);
|
||||||
|
if (history.length > recentAttemptDiversityWindow) {
|
||||||
|
history.removeAt(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _attemptSend(String messageId) async {
|
Future<void> _attemptSend(String messageId) async {
|
||||||
final message = _pendingMessages[messageId];
|
final message = _pendingMessages[messageId];
|
||||||
final contact = _pendingContacts[messageId];
|
final contact = _pendingContacts[messageId];
|
||||||
|
final config = _config;
|
||||||
|
|
||||||
if (message == null || contact == null) return;
|
if (message == null || contact == null || config == null) return;
|
||||||
|
|
||||||
|
final currentSelection = _selectPathForAttempt(message, contact);
|
||||||
|
|
||||||
|
if (currentSelection != null) {
|
||||||
|
final updatedMessage = message.copyWith(
|
||||||
|
pathLength: currentSelection.useFlood ? -1 : currentSelection.hopCount,
|
||||||
|
pathBytes: currentSelection.useFlood
|
||||||
|
? Uint8List(0)
|
||||||
|
: Uint8List.fromList(currentSelection.pathBytes),
|
||||||
|
);
|
||||||
|
_pendingMessages[messageId] = updatedMessage;
|
||||||
|
} else if (message.retryCount > 0) {
|
||||||
|
// No schedule entry for this retry — re-resolve path from current contact
|
||||||
|
// state so user's path override changes are picked up between retries.
|
||||||
|
final resolved = resolvePathSelection(contact);
|
||||||
|
final updatedMessage = message.copyWith(
|
||||||
|
pathLength: resolved.useFlood ? -1 : resolved.hopCount,
|
||||||
|
pathBytes: Uint8List.fromList(resolved.pathBytes),
|
||||||
|
);
|
||||||
|
_pendingMessages[messageId] = updatedMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-read after potential schedule update
|
||||||
|
final effectiveMessage = _pendingMessages[messageId] ?? message;
|
||||||
|
|
||||||
// Sync path settings with device before sending
|
// Sync path settings with device before sending
|
||||||
// Use the path that was captured when the message was first sent
|
if (config.setContactPath != null && config.clearContactPath != null) {
|
||||||
if (_setContactPathCallback != null && _clearContactPathCallback != null) {
|
final bool useFlood = currentSelection != null
|
||||||
if (message.pathLength != null && message.pathLength! < 0) {
|
? currentSelection.useFlood
|
||||||
// Flood mode - clear the path
|
: (effectiveMessage.pathLength != null &&
|
||||||
debugPrint(
|
effectiveMessage.pathLength! < 0);
|
||||||
'Setting flood mode for retry attempt ${message.retryCount}',
|
final List<int> pathBytes = currentSelection != null
|
||||||
);
|
? currentSelection.pathBytes
|
||||||
_clearContactPathCallback!(contact);
|
: effectiveMessage.pathBytes;
|
||||||
} else if (message.pathLength != null && message.pathLength! >= 0) {
|
final int hopCount = currentSelection != null
|
||||||
// Specific path (including direct neighbor with pathLength=0)
|
? currentSelection.hopCount
|
||||||
final pathStr = message.pathBytes.isEmpty
|
: (effectiveMessage.pathLength ?? 0);
|
||||||
? 'direct'
|
|
||||||
: message.pathBytes
|
if (useFlood) {
|
||||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
await config.clearContactPath!(contact);
|
||||||
.join(',');
|
} else if (effectiveMessage.pathLength != null) {
|
||||||
debugPrint(
|
await config.setContactPath!(
|
||||||
'Setting path [$pathStr] (${message.pathLength} hops) for retry attempt ${message.retryCount}',
|
|
||||||
);
|
|
||||||
await _setContactPathCallback!(
|
|
||||||
contact,
|
contact,
|
||||||
message.pathBytes,
|
Uint8List.fromList(pathBytes),
|
||||||
message.pathLength!,
|
hopCount,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final attempt = message.retryCount.clamp(0, 3);
|
// Re-validate after async gap — a timer or ACK could have resolved/retried
|
||||||
|
// this message while we were awaiting the path callback.
|
||||||
|
final currentMessage = _pendingMessages[messageId];
|
||||||
|
if (currentMessage == null || _resolvedMessages.contains(messageId)) {
|
||||||
|
debugPrint(
|
||||||
|
'_attemptSend: message $messageId resolved during path sync, aborting',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentMessage.retryCount != message.retryCount) {
|
||||||
|
debugPrint(
|
||||||
|
'_attemptSend: message $messageId retryCount changed during path sync, aborting',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentSelection != null) {
|
||||||
|
_recordAttemptPathHistory(messageId, currentSelection);
|
||||||
|
}
|
||||||
|
|
||||||
|
final attempt = message.retryCount;
|
||||||
final timestampSeconds = message.timestamp.millisecondsSinceEpoch ~/ 1000;
|
final timestampSeconds = message.timestamp.millisecondsSinceEpoch ~/ 1000;
|
||||||
|
|
||||||
// Compute expected ACK hash that device will return in RESP_CODE_SENT
|
// Compute expected ACK hash that device will return in RESP_CODE_SENT
|
||||||
// IMPORTANT: Use the transformed text (with SMAZ encoding if enabled) to match device's hash
|
// IMPORTANT: Use the transformed text (with SMAZ encoding if enabled) to match device's hash
|
||||||
final selfPubKey = _getSelfPublicKeyCallback?.call();
|
final selfPubKey = config.getSelfPublicKey?.call();
|
||||||
if (selfPubKey != null) {
|
if (selfPubKey != null) {
|
||||||
final outboundText =
|
final outboundText =
|
||||||
_prepareContactOutboundTextCallback?.call(contact, message.text) ??
|
config.prepareContactOutboundText?.call(contact, message.text) ??
|
||||||
message.text;
|
message.text;
|
||||||
final expectedHash = MessageRetryService.computeExpectedAckHash(
|
final expectedHash = MessageRetryService.computeExpectedAckHash(
|
||||||
timestampSeconds,
|
timestampSeconds,
|
||||||
@@ -208,42 +325,28 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
outboundText,
|
outboundText,
|
||||||
selfPubKey,
|
selfPubKey,
|
||||||
);
|
);
|
||||||
final expectedHashHex = expectedHash
|
final expectedHashHex = expectedHash.toRadixString(16).padLeft(8, '0');
|
||||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
|
||||||
.join();
|
|
||||||
_expectedHashToMessageId[expectedHashHex] = messageId;
|
_expectedHashToMessageId[expectedHashHex] = messageId;
|
||||||
|
|
||||||
final shortText = message.text.length > 20
|
final shortText = message.text.length > 20
|
||||||
? '${message.text.substring(0, 20)}...'
|
? '${message.text.substring(0, 20)}...'
|
||||||
: message.text;
|
: message.text;
|
||||||
_debugLogService?.info(
|
config.debugLogService?.info(
|
||||||
'Sent "$shortText" to ${contact.name} → expect ACK hash $expectedHashHex (attempt $attempt)',
|
'Sent "$shortText" to ${contact.name} → expect ACK hash $expectedHashHex (attempt $attempt)',
|
||||||
tag: 'AckHash',
|
tag: 'AckHash',
|
||||||
);
|
);
|
||||||
debugPrint(
|
|
||||||
'Computed expected ACK hash $expectedHashHex for message $messageId',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEPRECATED: Old queue-based matching (kept for fallback)
|
config.sendMessage(contact, message.text, attempt, timestampSeconds);
|
||||||
_pendingMessageQueuePerContact[contact.publicKeyHex] ??= [];
|
|
||||||
_pendingMessageQueuePerContact[contact.publicKeyHex]!.add(messageId);
|
|
||||||
|
|
||||||
if (_sendMessageCallback != null) {
|
|
||||||
_sendMessageCallback!(contact, message.text, attempt, timestampSeconds);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool updateMessageFromSent(
|
bool updateMessageFromSent(int ackHash, int timeoutMs) {
|
||||||
Uint8List ackHash,
|
final config = _config;
|
||||||
int timeoutMs, {
|
if (config == null) return false;
|
||||||
bool allowQueueFallback = true,
|
|
||||||
}) {
|
|
||||||
final ackHashHex = ackHash
|
|
||||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
|
||||||
.join();
|
|
||||||
|
|
||||||
// NEW: Try hash-based matching first (fixes LoRa message drops causing mismatches)
|
final ackHashHex = ackHash.toRadixString(16).padLeft(8, '0');
|
||||||
|
|
||||||
|
// Try hash-based matching (fixes LoRa message drops causing mismatches)
|
||||||
String? messageId = _expectedHashToMessageId.remove(ackHashHex);
|
String? messageId = _expectedHashToMessageId.remove(ackHashHex);
|
||||||
Contact? contact;
|
Contact? contact;
|
||||||
|
|
||||||
@@ -255,127 +358,51 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
final shortText = message.text.length > 20
|
final shortText = message.text.length > 20
|
||||||
? '${message.text.substring(0, 20)}...'
|
? '${message.text.substring(0, 20)}...'
|
||||||
: message.text;
|
: message.text;
|
||||||
_debugLogService?.info(
|
config.debugLogService?.info(
|
||||||
'RESP_CODE_SENT received: ACK hash $ackHashHex ✓ matched "$shortText" to ${contact.name}',
|
'RESP_CODE_SENT received: ACK hash $ackHashHex ✓ matched "$shortText" to ${contact.name}',
|
||||||
tag: 'AckHash',
|
tag: 'AckHash',
|
||||||
);
|
);
|
||||||
debugPrint(
|
|
||||||
'Hash-based match: ACK hash $ackHashHex → message $messageId ✓',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Remove from old queue since we matched
|
|
||||||
_pendingMessageQueuePerContact[contact.publicKeyHex]?.remove(messageId);
|
|
||||||
if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ??
|
|
||||||
false) {
|
|
||||||
_pendingMessageQueuePerContact.remove(contact.publicKeyHex);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
_debugLogService?.warn(
|
config.debugLogService?.warn(
|
||||||
'RESP_CODE_SENT: ACK hash $ackHashHex matched but message no longer pending',
|
'RESP_CODE_SENT: ACK hash $ackHashHex matched but message no longer pending',
|
||||||
tag: 'AckHash',
|
tag: 'AckHash',
|
||||||
);
|
);
|
||||||
debugPrint('Hash matched $messageId but message no longer pending');
|
|
||||||
messageId = null;
|
messageId = null;
|
||||||
contact = null;
|
contact = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FALLBACK: Old queue-based matching (for messages sent before hash computation was added)
|
|
||||||
if (messageId == null && allowQueueFallback) {
|
|
||||||
_debugLogService?.warn(
|
|
||||||
'RESP_CODE_SENT: ACK hash $ackHashHex not found in hash table, falling back to queue',
|
|
||||||
tag: 'AckHash',
|
|
||||||
);
|
|
||||||
debugPrint(
|
|
||||||
'Hash-based match failed for $ackHashHex, falling back to queue-based matching',
|
|
||||||
);
|
|
||||||
|
|
||||||
for (var entry in _pendingMessageQueuePerContact.entries) {
|
|
||||||
final contactKey = entry.key;
|
|
||||||
final queue = entry.value;
|
|
||||||
|
|
||||||
if (queue.isNotEmpty) {
|
|
||||||
final candidateMessageId = queue.removeAt(0);
|
|
||||||
|
|
||||||
if (_pendingMessages.containsKey(candidateMessageId)) {
|
|
||||||
messageId = candidateMessageId;
|
|
||||||
contact = _pendingContacts[candidateMessageId];
|
|
||||||
debugPrint(
|
|
||||||
'Queue-based match (fallback): $ackHashHex → message $messageId for $contactKey',
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
debugPrint('Dequeued stale message $candidateMessageId - skipping');
|
|
||||||
if (queue.isNotEmpty) {
|
|
||||||
final nextMessageId = queue.removeAt(0);
|
|
||||||
if (_pendingMessages.containsKey(nextMessageId)) {
|
|
||||||
messageId = nextMessageId;
|
|
||||||
contact = _pendingContacts[nextMessageId];
|
|
||||||
debugPrint(
|
|
||||||
'Queue-based match (fallback): $ackHashHex → message $messageId',
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the mapping for future lookups (e.g., when ACK arrives)
|
final message = _pendingMessages[messageId]!;
|
||||||
// Keep timestamp so we can clean up old mappings later
|
_ackHashToMessageId[ackHashHex] = (
|
||||||
_ackHashToMessageId[ackHashHex] = _AckHashMapping(
|
|
||||||
messageId: messageId,
|
messageId: messageId,
|
||||||
timestamp: DateTime.now(),
|
timestamp: DateTime.now(),
|
||||||
|
attemptIndex: message.retryCount,
|
||||||
);
|
);
|
||||||
debugPrint('Mapped ACK hash $ackHashHex to message $messageId');
|
|
||||||
|
|
||||||
final message = _pendingMessages[messageId];
|
|
||||||
final selection = _pendingPathSelections[messageId];
|
|
||||||
|
|
||||||
if (message == null) {
|
|
||||||
debugPrint(
|
|
||||||
'Message $messageId no longer pending for ACK hash: $ackHashHex',
|
|
||||||
);
|
|
||||||
_ackHashToMessageId.remove(ackHashHex);
|
|
||||||
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)
|
||||||
_expectedAckHashes[messageId] ??= [];
|
_expectedAckHashes[messageId] ??= [];
|
||||||
if (!_expectedAckHashes[messageId]!.any(
|
if (!_expectedAckHashes[messageId]!.any((hash) => hash == ackHash)) {
|
||||||
(hash) => listEquals(hash, ackHash),
|
_expectedAckHashes[messageId]!.add(ackHash);
|
||||||
)) {
|
|
||||||
_expectedAckHashes[messageId]!.add(Uint8List.fromList(ackHash));
|
|
||||||
debugPrint(
|
|
||||||
'Added ACK hash $ackHashHex to message $messageId (total: ${_expectedAckHashes[messageId]!.length})',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use device-provided timeout, or calculate from radio settings if timeout is 0 or invalid
|
// Calculate timeout: prefer ML prediction, then device-provided, then physics fallback
|
||||||
|
final pathLengthValue = message.pathLength ?? contact.pathLength;
|
||||||
|
|
||||||
int actualTimeout = timeoutMs;
|
int actualTimeout = timeoutMs;
|
||||||
if (timeoutMs <= 0 && _calculateTimeoutCallback != null) {
|
if (config.calculateTimeout != null) {
|
||||||
int pathLengthValue;
|
final calculated = config.calculateTimeout!(
|
||||||
if (selection != null) {
|
|
||||||
pathLengthValue = selection.useFlood ? -1 : selection.hopCount;
|
|
||||||
if (pathLengthValue < 0) pathLengthValue = contact.pathLength;
|
|
||||||
} else if (message.pathLength != null) {
|
|
||||||
pathLengthValue = message.pathLength!;
|
|
||||||
} else {
|
|
||||||
pathLengthValue = contact.pathLength;
|
|
||||||
}
|
|
||||||
actualTimeout = _calculateTimeoutCallback!(
|
|
||||||
pathLengthValue,
|
pathLengthValue,
|
||||||
message.text.length,
|
message.text.length,
|
||||||
|
contactKey: contact.publicKeyHex,
|
||||||
);
|
);
|
||||||
debugPrint(
|
if (timeoutMs <= 0 || calculated < timeoutMs) {
|
||||||
'Using calculated timeout: ${actualTimeout}ms for path length $pathLengthValue',
|
actualTimeout = calculated;
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final updatedMessage = message.copyWith(
|
final updatedMessage = message.copyWith(
|
||||||
@@ -386,18 +413,26 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
|
|
||||||
_pendingMessages[messageId] = updatedMessage;
|
_pendingMessages[messageId] = updatedMessage;
|
||||||
|
config.updateMessage(updatedMessage);
|
||||||
if (_updateMessageCallback != null) {
|
|
||||||
_updateMessageCallback!(updatedMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
_startTimeoutTimer(messageId, actualTimeout);
|
_startTimeoutTimer(messageId, actualTimeout);
|
||||||
debugPrint('Updated message $messageId with ACK hash: $ackHashHex');
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get hasPendingMessages => _pendingMessages.isNotEmpty;
|
bool get hasPendingMessages => _pendingMessages.isNotEmpty;
|
||||||
|
|
||||||
|
/// Update the stored contact snapshot for all pending messages to this contact.
|
||||||
|
/// Call this when the contact's pathOverride changes so retries use the new path.
|
||||||
|
void updatePendingContact(Contact contact) {
|
||||||
|
final keys = _pendingContacts.entries
|
||||||
|
.where((e) => e.value.publicKeyHex == contact.publicKeyHex)
|
||||||
|
.map((e) => e.key)
|
||||||
|
.toList();
|
||||||
|
for (final key in keys) {
|
||||||
|
_pendingContacts[key] = contact;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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), () {
|
||||||
@@ -405,10 +440,24 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _cleanupMessage(String messageId) {
|
||||||
|
_moveAckHashesToHistory(messageId);
|
||||||
|
_ackHashToMessageId.removeWhere(
|
||||||
|
(_, mapping) => mapping.messageId == messageId,
|
||||||
|
);
|
||||||
|
_expectedHashToMessageId.removeWhere((_, msgId) => msgId == messageId);
|
||||||
|
_pendingMessages.remove(messageId);
|
||||||
|
_pendingContacts.remove(messageId);
|
||||||
|
_attemptPathHistory.remove(messageId);
|
||||||
|
_timeoutTimers.remove(messageId);
|
||||||
|
_resolvedMessages.remove(messageId);
|
||||||
|
}
|
||||||
|
|
||||||
void _handleTimeout(String messageId) {
|
void _handleTimeout(String messageId) {
|
||||||
final message = _pendingMessages[messageId];
|
final message = _pendingMessages[messageId];
|
||||||
final contact = _pendingContacts[messageId];
|
final contact = _pendingContacts[messageId];
|
||||||
final selection = _pendingPathSelections[messageId];
|
final config = _config;
|
||||||
|
final selection = message != null ? _selectionFromMessage(message) : null;
|
||||||
|
|
||||||
if (message == null || contact == null) {
|
if (message == null || contact == null) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
@@ -420,70 +469,50 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
final shortText = message.text.length > 20
|
final shortText = message.text.length > 20
|
||||||
? '${message.text.substring(0, 20)}...'
|
? '${message.text.substring(0, 20)}...'
|
||||||
: message.text;
|
: message.text;
|
||||||
_debugLogService?.warn(
|
config?.debugLogService?.warn(
|
||||||
'Timeout: No ACK received for "$shortText" to ${contact.name} (attempt ${message.retryCount}) → retrying',
|
'Timeout: No ACK received for "$shortText" to ${contact.name} (attempt ${message.retryCount}) → retrying',
|
||||||
tag: 'AckHash',
|
tag: 'AckHash',
|
||||||
);
|
);
|
||||||
debugPrint(
|
|
||||||
'Timeout for message $messageId (retry ${message.retryCount}/${maxRetries - 1})',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (message.retryCount < maxRetries - 1) {
|
if (message.retryCount < maxRetries - 1) {
|
||||||
final backoffMs = 1000 * (1 << message.retryCount);
|
final backoffMs = 1000 * (1 << message.retryCount);
|
||||||
|
|
||||||
|
if (selection != null) {
|
||||||
|
_recordPathResultFromMessage(
|
||||||
|
contact.publicKeyHex,
|
||||||
|
message,
|
||||||
|
selection,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final updatedMessage = message.copyWith(
|
final updatedMessage = message.copyWith(
|
||||||
retryCount: message.retryCount + 1,
|
retryCount: message.retryCount + 1,
|
||||||
status: MessageStatus.pending,
|
status: MessageStatus.pending,
|
||||||
// Keep expectedAckHash - it will be updated when the new attempt is sent
|
|
||||||
);
|
);
|
||||||
|
|
||||||
_pendingMessages[messageId] = updatedMessage;
|
_pendingMessages[messageId] = updatedMessage;
|
||||||
|
config?.updateMessage(updatedMessage);
|
||||||
|
|
||||||
if (_updateMessageCallback != null) {
|
config?.debugLogService?.info(
|
||||||
_updateMessageCallback!(updatedMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
_debugLogService?.info(
|
|
||||||
'Scheduling retry for "$shortText" to ${contact.name} after ${backoffMs}ms backoff',
|
'Scheduling retry for "$shortText" to ${contact.name} after ${backoffMs}ms backoff',
|
||||||
tag: 'AckHash',
|
tag: 'AckHash',
|
||||||
);
|
);
|
||||||
debugPrint('Scheduling retry after ${backoffMs}ms');
|
|
||||||
|
|
||||||
// Store the backoff timer so it can be canceled if new RESP_CODE_SENT arrives
|
|
||||||
_timeoutTimers[messageId] = Timer(Duration(milliseconds: backoffMs), () {
|
_timeoutTimers[messageId] = Timer(Duration(milliseconds: backoffMs), () {
|
||||||
// Double-check message is still pending before retry
|
|
||||||
if (_pendingMessages.containsKey(messageId)) {
|
if (_pendingMessages.containsKey(messageId)) {
|
||||||
_attemptSend(messageId);
|
_attemptSend(messageId);
|
||||||
} else {
|
|
||||||
debugPrint(
|
|
||||||
'Retry cancelled: message $messageId was delivered while waiting',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Max retries reached - mark as failed
|
// Max retries reached - mark as failed
|
||||||
final failedMessage = message.copyWith(status: MessageStatus.failed);
|
final failedMessage = message.copyWith(status: MessageStatus.failed);
|
||||||
|
_pendingMessages[messageId] = failedMessage;
|
||||||
|
|
||||||
// Move ACK hashes to history before removing
|
if (config?.appSettingsService?.settings.clearPathOnMaxRetry == true &&
|
||||||
_moveAckHashesToHistory(messageId);
|
config?.clearContactPath != null) {
|
||||||
|
config!.clearContactPath!(contact);
|
||||||
_pendingMessages.remove(messageId);
|
|
||||||
_pendingContacts.remove(messageId);
|
|
||||||
_pendingPathSelections.remove(messageId);
|
|
||||||
_timeoutTimers[messageId]?.cancel();
|
|
||||||
_timeoutTimers.remove(messageId);
|
|
||||||
|
|
||||||
// Clean up the queue entry for this contact
|
|
||||||
_pendingMessageQueuePerContact[contact.publicKeyHex]?.remove(messageId);
|
|
||||||
if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ??
|
|
||||||
false) {
|
|
||||||
_pendingMessageQueuePerContact.remove(contact.publicKeyHex);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we should clear the path on max retry
|
|
||||||
if (_appSettingsService?.settings.clearPathOnMaxRetry == true &&
|
|
||||||
_clearContactPathCallback != null) {
|
|
||||||
_clearContactPathCallback!(contact);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_recordPathResultFromMessage(
|
_recordPathResultFromMessage(
|
||||||
@@ -494,11 +523,17 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (_updateMessageCallback != null) {
|
config?.updateMessage(failedMessage);
|
||||||
_updateMessageCallback!(failedMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
|
_onMessageResolved(messageId, contact.publicKeyHex);
|
||||||
|
|
||||||
|
// Keep message in pending maps for 30s grace period so late ACKs
|
||||||
|
// can still match and update the message to delivered.
|
||||||
|
_timeoutTimers[messageId] = Timer(const Duration(seconds: 30), () {
|
||||||
|
_cleanupMessage(messageId);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,24 +548,16 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Trim history to max size (rolling buffer)
|
|
||||||
while (_ackHistory.length > maxAckHistorySize) {
|
while (_ackHistory.length > maxAckHistorySize) {
|
||||||
_ackHistory.removeAt(0);
|
_ackHistory.removeAt(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint(
|
|
||||||
'Moved ${ackHashes.length} ACK hashes to history for message $messageId (history size: ${_ackHistory.length})',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _checkAckHistory(Uint8List ackHash) {
|
bool _checkAckHistory(int ackHash) {
|
||||||
for (final entry in _ackHistory) {
|
for (final entry in _ackHistory) {
|
||||||
for (final expectedHash in entry.ackHashes) {
|
for (final expectedHash in entry.ackHashes) {
|
||||||
if (listEquals(expectedHash, ackHash)) {
|
if (expectedHash == ackHash) {
|
||||||
debugPrint(
|
|
||||||
'Found ACK match in history: messageId=${entry.messageId}, age=${DateTime.now().difference(entry.timestamp).inSeconds}s',
|
|
||||||
);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -538,15 +565,13 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleAckReceived(Uint8List ackHash, int tripTimeMs) {
|
void handleAckReceived(int ackHash, int tripTimeMs) {
|
||||||
|
final config = _config;
|
||||||
String? matchedMessageId;
|
String? matchedMessageId;
|
||||||
final ackHashHex = ackHash
|
int? matchedAttemptIndex;
|
||||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
final ackHashHex = ackHash.toRadixString(16).padLeft(8, '0');
|
||||||
.join();
|
|
||||||
|
|
||||||
debugPrint('ACK received: $ackHashHex, trip time: ${tripTimeMs}ms');
|
// Clean up old ACK hash mappings (older than 15 minutes)
|
||||||
|
|
||||||
// First, clean up old ACK hash mappings (older than 15 minutes)
|
|
||||||
final cutoffTime = DateTime.now().subtract(const Duration(minutes: 15));
|
final cutoffTime = DateTime.now().subtract(const Duration(minutes: 15));
|
||||||
final hashesToRemove = <String>[];
|
final hashesToRemove = <String>[];
|
||||||
for (var entry in _ackHashToMessageId.entries) {
|
for (var entry in _ackHashToMessageId.entries) {
|
||||||
@@ -557,34 +582,26 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
for (var hash in hashesToRemove) {
|
for (var hash in hashesToRemove) {
|
||||||
_ackHashToMessageId.remove(hash);
|
_ackHashToMessageId.remove(hash);
|
||||||
}
|
}
|
||||||
if (hashesToRemove.isNotEmpty) {
|
|
||||||
debugPrint('Cleaned up ${hashesToRemove.length} old ACK hash mappings');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use direct O(1) lookup via ACK hash mapping
|
// Use direct O(1) lookup via ACK hash mapping
|
||||||
final mapping = _ackHashToMessageId[ackHashHex];
|
final mapping = _ackHashToMessageId[ackHashHex];
|
||||||
if (mapping != null) {
|
if (mapping != null) {
|
||||||
matchedMessageId = mapping.messageId;
|
matchedMessageId = mapping.messageId;
|
||||||
debugPrint('Matched ACK to message via direct lookup: $matchedMessageId');
|
matchedAttemptIndex = mapping.attemptIndex;
|
||||||
} else {
|
} else {
|
||||||
_debugLogService?.warn(
|
config?.debugLogService?.warn(
|
||||||
'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex not found in direct mapping, trying fallback',
|
'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex not found in direct mapping, trying fallback',
|
||||||
tag: 'AckHash',
|
tag: 'AckHash',
|
||||||
);
|
);
|
||||||
// Fallback: Check against ALL expected ACK hashes (from all retry attempts)
|
// Fallback: Check against ALL expected ACK hashes (from all retry attempts)
|
||||||
debugPrint(
|
|
||||||
'ACK not in mapping, checking _expectedAckHashes (${_expectedAckHashes.length} messages)',
|
|
||||||
);
|
|
||||||
for (var entry in _expectedAckHashes.entries) {
|
for (var entry in _expectedAckHashes.entries) {
|
||||||
final messageId = entry.key;
|
final messageId = entry.key;
|
||||||
final expectedHashes = entry.value;
|
final expectedHashes = entry.value;
|
||||||
|
|
||||||
for (final expectedHash in expectedHashes) {
|
for (final expectedHash in expectedHashes) {
|
||||||
if (listEquals(expectedHash, ackHash)) {
|
if (expectedHash == ackHash) {
|
||||||
matchedMessageId = messageId;
|
matchedMessageId = messageId;
|
||||||
debugPrint(
|
matchedAttemptIndex = expectedHashes.indexOf(expectedHash);
|
||||||
'Matched ACK to message via fallback: $matchedMessageId (attempt ${expectedHashes.indexOf(expectedHash)})',
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -594,21 +611,24 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (matchedMessageId != null) {
|
if (matchedMessageId != null) {
|
||||||
final message = _pendingMessages[matchedMessageId]!;
|
final message = _pendingMessages[matchedMessageId];
|
||||||
|
if (message == null) {
|
||||||
|
_ackHashToMessageId.remove(ackHashHex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
final contact = _pendingContacts[matchedMessageId];
|
final contact = _pendingContacts[matchedMessageId];
|
||||||
final selection = _pendingPathSelections[matchedMessageId];
|
final ackedAttempt = matchedAttemptIndex ?? message.retryCount;
|
||||||
|
final selection = _selectionFromMessage(message);
|
||||||
|
|
||||||
final shortText = message.text.length > 20
|
final shortText = message.text.length > 20
|
||||||
? '${message.text.substring(0, 20)}...'
|
? '${message.text.substring(0, 20)}...'
|
||||||
: message.text;
|
: message.text;
|
||||||
_debugLogService?.info(
|
config?.debugLogService?.info(
|
||||||
'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex ✓ "$shortText" delivered to ${contact?.name ?? "unknown"} in ${tripTimeMs}ms',
|
'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex ✓ "$shortText" delivered to ${contact?.name ?? "unknown"} on retry ${ackedAttempt + 1} in ${tripTimeMs}ms',
|
||||||
tag: 'AckHash',
|
tag: 'AckHash',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Cancel any pending timeout or retry
|
|
||||||
_timeoutTimers[matchedMessageId]?.cancel();
|
_timeoutTimers[matchedMessageId]?.cancel();
|
||||||
_timeoutTimers.remove(matchedMessageId);
|
|
||||||
|
|
||||||
final deliveredMessage = message.copyWith(
|
final deliveredMessage = message.copyWith(
|
||||||
status: MessageStatus.delivered,
|
status: MessageStatus.delivered,
|
||||||
@@ -616,27 +636,9 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
tripTimeMs: tripTimeMs,
|
tripTimeMs: tripTimeMs,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Move ACK hashes to history before removing
|
_cleanupMessage(matchedMessageId);
|
||||||
_moveAckHashesToHistory(matchedMessageId);
|
|
||||||
|
|
||||||
_pendingMessages.remove(matchedMessageId);
|
config?.updateMessage(deliveredMessage);
|
||||||
_pendingContacts.remove(matchedMessageId);
|
|
||||||
_pendingPathSelections.remove(matchedMessageId);
|
|
||||||
|
|
||||||
// Clean up the queue entry for this contact (remove any remaining references to this message)
|
|
||||||
if (contact != null) {
|
|
||||||
_pendingMessageQueuePerContact[contact.publicKeyHex]?.remove(
|
|
||||||
matchedMessageId,
|
|
||||||
);
|
|
||||||
if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ??
|
|
||||||
false) {
|
|
||||||
_pendingMessageQueuePerContact.remove(contact.publicKeyHex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_updateMessageCallback != null) {
|
|
||||||
_updateMessageCallback!(deliveredMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contact != null) {
|
if (contact != null) {
|
||||||
_recordPathResultFromMessage(
|
_recordPathResultFromMessage(
|
||||||
@@ -646,19 +648,28 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
true,
|
true,
|
||||||
tripTimeMs,
|
tripTimeMs,
|
||||||
);
|
);
|
||||||
|
if (config?.onDeliveryObserved != null &&
|
||||||
|
tripTimeMs > 0 &&
|
||||||
|
message.pathLength != null) {
|
||||||
|
config!.onDeliveryObserved!(
|
||||||
|
contact.publicKeyHex,
|
||||||
|
message.pathLength!,
|
||||||
|
message.text.length,
|
||||||
|
tripTimeMs,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_onMessageResolved(matchedMessageId, contact.publicKeyHex);
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} else {
|
} else {
|
||||||
// Check ACK history for recently completed messages
|
|
||||||
if (_checkAckHistory(ackHash)) {
|
if (_checkAckHistory(ackHash)) {
|
||||||
_debugLogService?.info(
|
config?.debugLogService?.info(
|
||||||
'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex matched a recently completed message (duplicate ACK)',
|
'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex matched a recently completed message (duplicate ACK)',
|
||||||
tag: 'AckHash',
|
tag: 'AckHash',
|
||||||
);
|
);
|
||||||
debugPrint('ACK matched a recently completed message from history');
|
|
||||||
} else {
|
} else {
|
||||||
_debugLogService?.error(
|
config?.debugLogService?.error(
|
||||||
'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex has no matching message!',
|
'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex has no matching message!',
|
||||||
tag: 'AckHash',
|
tag: 'AckHash',
|
||||||
);
|
);
|
||||||
@@ -667,62 +678,11 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint8List _resolveMessagePathBytes(
|
String? getContactKeyForAckHash(int ackHash) {
|
||||||
Contact contact,
|
|
||||||
bool forceFlood,
|
|
||||||
PathSelection? selection,
|
|
||||||
) {
|
|
||||||
// Priority 1: Check user's path override
|
|
||||||
if (contact.pathOverride != null) {
|
|
||||||
if (contact.pathOverride! < 0) {
|
|
||||||
return Uint8List(0); // Force flood
|
|
||||||
}
|
|
||||||
return contact.pathOverrideBytes ?? Uint8List(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 2: Check forceFlood or device flood mode
|
|
||||||
if (forceFlood || contact.pathLength < 0 || selection?.useFlood == true) {
|
|
||||||
return Uint8List(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 3: Check PathSelection (auto-rotation)
|
|
||||||
if (selection != null && selection.pathBytes.isNotEmpty) {
|
|
||||||
return Uint8List.fromList(selection.pathBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 4: Use device's discovered path
|
|
||||||
return contact.path;
|
|
||||||
}
|
|
||||||
|
|
||||||
int? _resolveMessagePathLength(
|
|
||||||
Contact contact,
|
|
||||||
bool forceFlood,
|
|
||||||
PathSelection? selection,
|
|
||||||
) {
|
|
||||||
// Priority 1: Check user's path override
|
|
||||||
if (contact.pathOverride != null) {
|
|
||||||
return contact.pathOverride;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 2: Check forceFlood or device flood mode
|
|
||||||
if (forceFlood || contact.pathLength < 0 || selection?.useFlood == true) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 3: Check PathSelection (auto-rotation)
|
|
||||||
if (selection != null && selection.pathBytes.isNotEmpty) {
|
|
||||||
return selection.hopCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 4: Use device's discovered path
|
|
||||||
return contact.pathLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
String? getContactKeyForAckHash(Uint8List ackHash) {
|
|
||||||
for (var entry in _pendingMessages.entries) {
|
for (var entry in _pendingMessages.entries) {
|
||||||
final message = entry.value;
|
final message = entry.value;
|
||||||
if (message.expectedAckHash != null &&
|
if (message.expectedAckHash != null &&
|
||||||
listEquals(message.expectedAckHash, ackHash)) {
|
message.expectedAckHash == ackHash) {
|
||||||
final contact = _pendingContacts[entry.key];
|
final contact = _pendingContacts[entry.key];
|
||||||
return contact?.publicKeyHex;
|
return contact?.publicKeyHex;
|
||||||
}
|
}
|
||||||
@@ -745,15 +705,11 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
bool success,
|
bool success,
|
||||||
int? tripTimeMs,
|
int? tripTimeMs,
|
||||||
) {
|
) {
|
||||||
if (_recordPathResultCallback == null) return;
|
final callback = _config?.recordPathResult;
|
||||||
|
if (callback == null) return;
|
||||||
final recordSelection = selection ?? _selectionFromMessage(message);
|
final recordSelection = selection ?? _selectionFromMessage(message);
|
||||||
if (recordSelection == null) return;
|
if (recordSelection == null) return;
|
||||||
_recordPathResultCallback!(
|
callback(contactKey, recordSelection, success, tripTimeMs);
|
||||||
contactKey,
|
|
||||||
recordSelection,
|
|
||||||
success,
|
|
||||||
tripTimeMs,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PathSelection? _selectionFromMessage(Message message) {
|
PathSelection? _selectionFromMessage(Message message) {
|
||||||
@@ -778,11 +734,13 @@ class MessageRetryService extends ChangeNotifier {
|
|||||||
_timeoutTimers.clear();
|
_timeoutTimers.clear();
|
||||||
_pendingMessages.clear();
|
_pendingMessages.clear();
|
||||||
_pendingContacts.clear();
|
_pendingContacts.clear();
|
||||||
_pendingPathSelections.clear();
|
_attemptPathHistory.clear();
|
||||||
_expectedAckHashes.clear();
|
_expectedAckHashes.clear();
|
||||||
_ackHistory.clear();
|
_ackHistory.clear();
|
||||||
_ackHashToMessageId.clear();
|
_ackHashToMessageId.clear();
|
||||||
_pendingMessageQueuePerContact.clear();
|
_sendQueue.clear();
|
||||||
|
_activeMessages.clear();
|
||||||
|
_resolvedMessages.clear();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
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 '../helpers/reaction_helper.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();
|
||||||
@@ -63,14 +66,27 @@ class NotificationService {
|
|||||||
appUserModelId: 'org.meshcore.open.app',
|
appUserModelId: 'org.meshcore.open.app',
|
||||||
guid: 'e7ea8f85-72f5-4f36-91f6-038f740ccf86',
|
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,
|
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,
|
||||||
@@ -82,6 +98,15 @@ 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 {
|
Future<bool> _ensureInitialized() async {
|
||||||
if (!_isInitialized) {
|
if (!_isInitialized) {
|
||||||
await initialize();
|
await initialize();
|
||||||
@@ -121,6 +146,19 @@ class NotificationService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Format special message types for human-readable notifications.
|
||||||
|
static String formatNotificationText(String text) {
|
||||||
|
final trimmed = text.trim();
|
||||||
|
final reaction = ReactionHelper.parseReaction(trimmed);
|
||||||
|
if (reaction != null) {
|
||||||
|
return 'Reacted ${reaction.emoji}';
|
||||||
|
}
|
||||||
|
if (RegExp(r'^g:[A-Za-z0-9_-]+$').hasMatch(trimmed)) {
|
||||||
|
return 'Sent a GIF';
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _showMessageNotificationImpl({
|
Future<void> _showMessageNotificationImpl({
|
||||||
required String contactName,
|
required String contactName,
|
||||||
required String message,
|
required String message,
|
||||||
@@ -163,7 +201,7 @@ class NotificationService {
|
|||||||
await _notifications.show(
|
await _notifications.show(
|
||||||
id: contactId?.hashCode ?? 0,
|
id: contactId?.hashCode ?? 0,
|
||||||
title: contactName,
|
title: contactName,
|
||||||
body: message,
|
body: formatNotificationText(message),
|
||||||
notificationDetails: notificationDetails,
|
notificationDetails: notificationDetails,
|
||||||
payload: 'message:$contactId',
|
payload: 'message:$contactId',
|
||||||
);
|
);
|
||||||
@@ -208,7 +246,9 @@ class NotificationService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await _notifications.show(
|
await _notifications.show(
|
||||||
id: contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
|
id: contactId != null
|
||||||
|
? 'advert:$contactId'.hashCode
|
||||||
|
: DateTime.now().millisecondsSinceEpoch,
|
||||||
title: _l10n.notification_newTypeDiscovered(contactType),
|
title: _l10n.notification_newTypeDiscovered(contactType),
|
||||||
body: contactName,
|
body: contactName,
|
||||||
notificationDetails: notificationDetails,
|
notificationDetails: notificationDetails,
|
||||||
@@ -257,7 +297,7 @@ class NotificationService {
|
|||||||
macOS: macDetails,
|
macOS: macDetails,
|
||||||
);
|
);
|
||||||
|
|
||||||
final preview = message.trim();
|
final preview = formatNotificationText(message.trim());
|
||||||
final body = preview.isEmpty
|
final body = preview.isEmpty
|
||||||
? _l10n.notification_receivedNewMessage
|
? _l10n.notification_receivedNewMessage
|
||||||
: preview;
|
: preview;
|
||||||
@@ -307,6 +347,61 @@ class NotificationService {
|
|||||||
await _notifications.cancel(id: id);
|
await _notifications.cancel(id: id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cancel the notification for a specific contact and update the app badge.
|
||||||
|
Future<void> clearContactNotification(
|
||||||
|
String contactId,
|
||||||
|
int totalUnreadCount,
|
||||||
|
) async {
|
||||||
|
if (!await _ensureInitialized()) return;
|
||||||
|
await _notifications.cancel(id: contactId.hashCode);
|
||||||
|
await _updateBadge(totalUnreadCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cancel the notification for a specific channel and update the app badge.
|
||||||
|
Future<void> clearChannelNotification(
|
||||||
|
int channelIndex,
|
||||||
|
int totalUnreadCount,
|
||||||
|
) async {
|
||||||
|
if (!await _ensureInitialized()) return;
|
||||||
|
await _notifications.cancel(id: channelIndex.hashCode);
|
||||||
|
await _updateBadge(totalUnreadCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cancel advert notifications for the given contact public key hexes.
|
||||||
|
Future<void> clearAdvertNotifications(List<String> contactIds) async {
|
||||||
|
if (!await _ensureInitialized()) return;
|
||||||
|
for (final id in contactIds) {
|
||||||
|
await _notifications.cancel(id: 'advert:$id'.hashCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _updateBadge(int count) async {
|
||||||
|
if (PlatformInfo.isIOS || PlatformInfo.isMacOS) {
|
||||||
|
// On Apple platforms, set the badge number directly via a silent update.
|
||||||
|
final darwinDetails = DarwinNotificationDetails(
|
||||||
|
presentAlert: false,
|
||||||
|
presentSound: false,
|
||||||
|
presentBadge: true,
|
||||||
|
badgeNumber: count,
|
||||||
|
);
|
||||||
|
final details = NotificationDetails(
|
||||||
|
iOS: darwinDetails,
|
||||||
|
macOS: darwinDetails,
|
||||||
|
);
|
||||||
|
// Use a fixed ID so each update replaces the previous one.
|
||||||
|
await _notifications.show(
|
||||||
|
id: 'badge_update'.hashCode,
|
||||||
|
title: null,
|
||||||
|
body: null,
|
||||||
|
notificationDetails: details,
|
||||||
|
);
|
||||||
|
// Immediately cancel the silent notification so it doesn't appear in tray.
|
||||||
|
await _notifications.cancel(id: 'badge_update'.hashCode);
|
||||||
|
}
|
||||||
|
// On Android, badge count is derived from active notifications,
|
||||||
|
// so cancelling the specific notification above is sufficient.
|
||||||
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────
|
||||||
// Public notification methods (rate limiting is enforced automatically)
|
// Public notification methods (rate limiting is enforced automatically)
|
||||||
// ─────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────
|
||||||
@@ -349,6 +444,7 @@ class NotificationService {
|
|||||||
|
|
||||||
Future<void> showChannelMessageNotification({
|
Future<void> showChannelMessageNotification({
|
||||||
required String channelName,
|
required String channelName,
|
||||||
|
required String senderName,
|
||||||
required String message,
|
required String message,
|
||||||
int? channelIndex,
|
int? channelIndex,
|
||||||
int? badgeCount,
|
int? badgeCount,
|
||||||
@@ -359,7 +455,7 @@ class NotificationService {
|
|||||||
_PendingNotification(
|
_PendingNotification(
|
||||||
type: _NotificationType.channelMessage,
|
type: _NotificationType.channelMessage,
|
||||||
title: channelName,
|
title: channelName,
|
||||||
body: message,
|
body: '$senderName: $message',
|
||||||
id: channelIndex?.toString(),
|
id: channelIndex?.toString(),
|
||||||
badgeCount: badgeCount,
|
badgeCount: badgeCount,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -9,13 +9,17 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
final Map<String, ContactPathHistory> _cache = {};
|
final Map<String, ContactPathHistory> _cache = {};
|
||||||
final Map<String, int> _autoRotationIndex = {};
|
final Map<String, int> _autoRotationIndex = {};
|
||||||
final Map<String, _FloodStats> _floodStats = {};
|
final Map<String, _FloodStats> _floodStats = {};
|
||||||
|
final Set<String> _pendingLoads = {};
|
||||||
|
final Map<String, List<_DeferredPathRecord>> _deferredRecords = {};
|
||||||
|
|
||||||
// LRU cache eviction tracking
|
// LRU cache eviction tracking
|
||||||
static const int _maxCachedContacts = 50;
|
static const int _maxCachedContacts = 50;
|
||||||
final List<String> _cacheAccessOrder = [];
|
final List<String> _cacheAccessOrder = [];
|
||||||
|
|
||||||
static const int _maxHistoryEntries = 100;
|
static const int _maxHistoryEntries = 100;
|
||||||
static const int _autoRotationTopCount = 3;
|
|
||||||
|
int _version = 0;
|
||||||
|
int get version => _version;
|
||||||
|
|
||||||
PathHistoryService(this._storage);
|
PathHistoryService(this._storage);
|
||||||
|
|
||||||
@@ -23,17 +27,21 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
// Load cached path histories on startup if needed
|
// Load cached path histories on startup if needed
|
||||||
}
|
}
|
||||||
|
|
||||||
void handlePathUpdated(Contact contact) {
|
void handlePathUpdated(Contact contact, {double initialWeight = 1.0}) {
|
||||||
if (contact.pathLength < 0) return;
|
if (contact.pathLength < 0 && contact.path.isEmpty) return;
|
||||||
|
final hopCount = contact.pathLength < 0
|
||||||
|
? contact.path.length
|
||||||
|
: contact.pathLength;
|
||||||
_addPathRecord(
|
_addPathRecord(
|
||||||
contactPubKeyHex: contact.publicKeyHex,
|
contactPubKeyHex: contact.publicKeyHex,
|
||||||
hopCount: contact.pathLength,
|
hopCount: hopCount,
|
||||||
tripTimeMs: 0,
|
tripTimeMs: 0,
|
||||||
wasFloodDiscovery: true,
|
wasFloodDiscovery: true,
|
||||||
pathBytes: contact.path,
|
pathBytes: contact.path,
|
||||||
successCount: 0,
|
successCount: 0,
|
||||||
failureCount: 0,
|
failureCount: 0,
|
||||||
|
routeWeight: initialWeight,
|
||||||
|
timestamp: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +59,44 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
pathBytes: selection.pathBytes,
|
pathBytes: selection.pathBytes,
|
||||||
successCount: 0,
|
successCount: 0,
|
||||||
failureCount: 0,
|
failureCount: 0,
|
||||||
|
timestamp: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When a flood message is delivered, credit the contact's current device
|
||||||
|
/// path so that the route the ACK traveled back through gets a weight boost.
|
||||||
|
void recordFloodPathAttribution({
|
||||||
|
required String contactPubKeyHex,
|
||||||
|
required List<int> pathBytes,
|
||||||
|
required int hopCount,
|
||||||
|
int? tripTimeMs,
|
||||||
|
double successIncrement = 0.5,
|
||||||
|
double maxWeight = 5.0,
|
||||||
|
}) {
|
||||||
|
if (pathBytes.isEmpty || hopCount < 0) return;
|
||||||
|
|
||||||
|
final existing = _findPathRecord(contactPubKeyHex, pathBytes);
|
||||||
|
final successCount = (existing?.successCount ?? 0) + 1;
|
||||||
|
final failureCount = existing?.failureCount ?? 0;
|
||||||
|
|
||||||
|
final currentWeight = existing?.routeWeight ?? 1.0;
|
||||||
|
final newWeight = (currentWeight + successIncrement).clamp(0.0, maxWeight);
|
||||||
|
|
||||||
|
debugPrint(
|
||||||
|
'Flood path attribution: crediting path [${pathBytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(',')}] '
|
||||||
|
'for $contactPubKeyHex (weight $currentWeight → $newWeight)',
|
||||||
|
);
|
||||||
|
|
||||||
|
_addPathRecord(
|
||||||
|
contactPubKeyHex: contactPubKeyHex,
|
||||||
|
hopCount: hopCount,
|
||||||
|
tripTimeMs: tripTimeMs ?? existing?.tripTimeMs ?? 0,
|
||||||
|
wasFloodDiscovery: true,
|
||||||
|
pathBytes: pathBytes,
|
||||||
|
successCount: successCount,
|
||||||
|
failureCount: failureCount,
|
||||||
|
routeWeight: newWeight,
|
||||||
|
timestamp: DateTime.now(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +105,9 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
PathSelection selection, {
|
PathSelection selection, {
|
||||||
required bool success,
|
required bool success,
|
||||||
int? tripTimeMs,
|
int? tripTimeMs,
|
||||||
|
double successIncrement = 0.5,
|
||||||
|
double failureDecrement = 0.5,
|
||||||
|
double maxWeight = 5.0,
|
||||||
}) {
|
}) {
|
||||||
if (selection.useFlood) {
|
if (selection.useFlood) {
|
||||||
final stats = _floodStats.putIfAbsent(
|
final stats = _floodStats.putIfAbsent(
|
||||||
@@ -79,6 +128,18 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
final successCount = (existing?.successCount ?? 0) + (success ? 1 : 0);
|
final successCount = (existing?.successCount ?? 0) + (success ? 1 : 0);
|
||||||
final failureCount = (existing?.failureCount ?? 0) + (success ? 0 : 1);
|
final failureCount = (existing?.failureCount ?? 0) + (success ? 0 : 1);
|
||||||
|
|
||||||
|
final currentWeight = existing?.routeWeight ?? 1.0;
|
||||||
|
double newWeight;
|
||||||
|
if (success) {
|
||||||
|
newWeight = (currentWeight + successIncrement).clamp(0.0, maxWeight);
|
||||||
|
} else {
|
||||||
|
newWeight = currentWeight - failureDecrement;
|
||||||
|
if (newWeight <= 0) {
|
||||||
|
removePathRecord(contactPubKeyHex, selection.pathBytes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_addPathRecord(
|
_addPathRecord(
|
||||||
contactPubKeyHex: contactPubKeyHex,
|
contactPubKeyHex: contactPubKeyHex,
|
||||||
hopCount: selection.hopCount,
|
hopCount: selection.hopCount,
|
||||||
@@ -87,37 +148,68 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
pathBytes: selection.pathBytes,
|
pathBytes: selection.pathBytes,
|
||||||
successCount: successCount,
|
successCount: successCount,
|
||||||
failureCount: failureCount,
|
failureCount: failureCount,
|
||||||
|
routeWeight: newWeight,
|
||||||
|
timestamp: success ? DateTime.now() : existing?.timestamp,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
PathSelection getNextAutoPathSelection(String contactPubKeyHex) {
|
PathSelection selectPathForAttempt(
|
||||||
final ranked = _getRankedPaths(
|
String contactPubKeyHex, {
|
||||||
contactPubKeyHex,
|
required int attemptIndex,
|
||||||
).take(_autoRotationTopCount).toList();
|
required int maxRetries,
|
||||||
|
List<PathSelection> recentSelections = const [],
|
||||||
|
}) {
|
||||||
|
if (maxRetries <= 0 || attemptIndex >= maxRetries - 1) {
|
||||||
|
return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ranked = _getRankedPaths(contactPubKeyHex);
|
||||||
if (ranked.isEmpty) {
|
if (ranked.isEmpty) {
|
||||||
return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true);
|
return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
_trackAccess(contactPubKeyHex);
|
_trackAccess(contactPubKeyHex);
|
||||||
|
|
||||||
final selections =
|
final recentPaths = recentSelections
|
||||||
ranked
|
.where((selection) => !selection.useFlood)
|
||||||
.map(
|
.map((selection) => selection.pathBytes)
|
||||||
(path) => PathSelection(
|
.toList();
|
||||||
pathBytes: path.pathBytes,
|
final candidates = recentPaths.isEmpty
|
||||||
hopCount: path.hopCount,
|
? ranked
|
||||||
useFlood: false,
|
: ranked
|
||||||
|
.where(
|
||||||
|
(path) => !recentPaths.any(
|
||||||
|
(recentPath) => _pathsEqual(path.pathBytes, recentPath),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList()
|
.toList();
|
||||||
..add(
|
final selected = candidates.isNotEmpty
|
||||||
const PathSelection(pathBytes: [], hopCount: -1, useFlood: true),
|
? (recentPaths.isEmpty
|
||||||
|
? _selectRotatedCandidate(contactPubKeyHex, candidates)
|
||||||
|
: candidates.first)
|
||||||
|
: ranked.first;
|
||||||
|
|
||||||
|
return PathSelection(
|
||||||
|
pathBytes: selected.pathBytes,
|
||||||
|
hopCount: selected.hopCount,
|
||||||
|
useFlood: false,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PathRecord _selectRotatedCandidate(
|
||||||
|
String contactPubKeyHex,
|
||||||
|
List<PathRecord> candidates,
|
||||||
|
) {
|
||||||
|
if (candidates.length <= 1) {
|
||||||
|
_autoRotationIndex[contactPubKeyHex] = 0;
|
||||||
|
return candidates.first;
|
||||||
|
}
|
||||||
|
|
||||||
final currentIndex = _autoRotationIndex[contactPubKeyHex] ?? 0;
|
final currentIndex = _autoRotationIndex[contactPubKeyHex] ?? 0;
|
||||||
final selection = selections[currentIndex % selections.length];
|
final selectedIndex = currentIndex % candidates.length;
|
||||||
_autoRotationIndex[contactPubKeyHex] = currentIndex + 1;
|
_autoRotationIndex[contactPubKeyHex] =
|
||||||
return selection;
|
(selectedIndex + 1) % candidates.length;
|
||||||
|
return candidates[selectedIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
void _addPathRecord({
|
void _addPathRecord({
|
||||||
@@ -128,24 +220,35 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
required List<int> pathBytes,
|
required List<int> pathBytes,
|
||||||
required int successCount,
|
required int successCount,
|
||||||
required int failureCount,
|
required int failureCount,
|
||||||
|
double routeWeight = 1.0,
|
||||||
|
DateTime? timestamp,
|
||||||
}) {
|
}) {
|
||||||
var history = _cache[contactPubKeyHex];
|
var history = _cache[contactPubKeyHex];
|
||||||
|
|
||||||
if (history == null) {
|
if (history == null) {
|
||||||
_loadHistoryFromStorage(contactPubKeyHex).then((loaded) {
|
// If a load is already in progress, defer this record
|
||||||
if (loaded != null) {
|
if (_pendingLoads.contains(contactPubKeyHex)) {
|
||||||
_cache[contactPubKeyHex] = loaded;
|
_deferredRecords.putIfAbsent(contactPubKeyHex, () => []);
|
||||||
_addPathRecordInternal(
|
_deferredRecords[contactPubKeyHex]!.add(
|
||||||
contactPubKeyHex,
|
_DeferredPathRecord(
|
||||||
hopCount,
|
hopCount: hopCount,
|
||||||
tripTimeMs,
|
tripTimeMs: tripTimeMs,
|
||||||
wasFloodDiscovery,
|
wasFloodDiscovery: wasFloodDiscovery,
|
||||||
pathBytes,
|
pathBytes: pathBytes,
|
||||||
successCount,
|
successCount: successCount,
|
||||||
failureCount,
|
failureCount: failureCount,
|
||||||
|
routeWeight: routeWeight,
|
||||||
|
timestamp: timestamp,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
return;
|
||||||
_cache[contactPubKeyHex] = ContactPathHistory(
|
}
|
||||||
|
|
||||||
|
_pendingLoads.add(contactPubKeyHex);
|
||||||
|
_loadHistoryFromStorage(contactPubKeyHex).then((loaded) {
|
||||||
|
_cache[contactPubKeyHex] =
|
||||||
|
loaded ??
|
||||||
|
ContactPathHistory(
|
||||||
contactPubKeyHex: contactPubKeyHex,
|
contactPubKeyHex: contactPubKeyHex,
|
||||||
recentPaths: [],
|
recentPaths: [],
|
||||||
);
|
);
|
||||||
@@ -157,8 +260,28 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
pathBytes,
|
pathBytes,
|
||||||
successCount,
|
successCount,
|
||||||
failureCount,
|
failureCount,
|
||||||
|
routeWeight,
|
||||||
|
timestamp,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply any deferred records
|
||||||
|
final deferred = _deferredRecords.remove(contactPubKeyHex);
|
||||||
|
if (deferred != null) {
|
||||||
|
for (final record in deferred) {
|
||||||
|
_addPathRecordInternal(
|
||||||
|
contactPubKeyHex,
|
||||||
|
record.hopCount,
|
||||||
|
record.tripTimeMs,
|
||||||
|
record.wasFloodDiscovery,
|
||||||
|
record.pathBytes,
|
||||||
|
record.successCount,
|
||||||
|
record.failureCount,
|
||||||
|
record.routeWeight,
|
||||||
|
record.timestamp,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
_pendingLoads.remove(contactPubKeyHex);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -171,6 +294,8 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
pathBytes,
|
pathBytes,
|
||||||
successCount,
|
successCount,
|
||||||
failureCount,
|
failureCount,
|
||||||
|
routeWeight,
|
||||||
|
timestamp,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,9 +307,12 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
List<int> pathBytes,
|
List<int> pathBytes,
|
||||||
int successCount,
|
int successCount,
|
||||||
int failureCount,
|
int failureCount,
|
||||||
|
double routeWeight,
|
||||||
|
DateTime? timestamp,
|
||||||
) {
|
) {
|
||||||
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) {
|
||||||
@@ -194,16 +322,18 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
tripTimeMs = existing.tripTimeMs;
|
tripTimeMs = existing.tripTimeMs;
|
||||||
}
|
}
|
||||||
wasFloodDiscovery = existing.wasFloodDiscovery || wasFloodDiscovery;
|
wasFloodDiscovery = existing.wasFloodDiscovery || wasFloodDiscovery;
|
||||||
|
timestamp ??= existing.timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
final newRecord = PathRecord(
|
final newRecord = PathRecord(
|
||||||
hopCount: hopCount,
|
hopCount: hopCount,
|
||||||
tripTimeMs: tripTimeMs,
|
tripTimeMs: tripTimeMs,
|
||||||
timestamp: DateTime.now(),
|
timestamp: timestamp,
|
||||||
wasFloodDiscovery: wasFloodDiscovery,
|
wasFloodDiscovery: wasFloodDiscovery,
|
||||||
pathBytes: pathBytes,
|
pathBytes: pathBytes,
|
||||||
successCount: successCount,
|
successCount: successCount,
|
||||||
failureCount: failureCount,
|
failureCount: failureCount,
|
||||||
|
routeWeight: routeWeight,
|
||||||
);
|
);
|
||||||
|
|
||||||
final updatedPaths = List<PathRecord>.from(history.recentPaths);
|
final updatedPaths = List<PathRecord>.from(history.recentPaths);
|
||||||
@@ -241,6 +371,7 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
_cache[contactPubKeyHex] = loaded;
|
_cache[contactPubKeyHex] = loaded;
|
||||||
_trackAccess(contactPubKeyHex);
|
_trackAccess(contactPubKeyHex);
|
||||||
_evictIfNeeded();
|
_evictIfNeeded();
|
||||||
|
_version++;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -270,12 +401,30 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
return history?.mostRecent;
|
return history?.mostRecent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
({
|
||||||
|
int successCount,
|
||||||
|
int failureCount,
|
||||||
|
int lastTripTimeMs,
|
||||||
|
DateTime? lastUsed,
|
||||||
|
})?
|
||||||
|
getFloodStats(String contactPubKeyHex) {
|
||||||
|
final stats = _floodStats[contactPubKeyHex];
|
||||||
|
if (stats == null) return null;
|
||||||
|
return (
|
||||||
|
successCount: stats.successCount,
|
||||||
|
failureCount: stats.failureCount,
|
||||||
|
lastTripTimeMs: stats.lastTripTimeMs,
|
||||||
|
lastUsed: stats.lastUsed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> clearPathHistory(String contactPubKeyHex) async {
|
Future<void> clearPathHistory(String contactPubKeyHex) async {
|
||||||
_cache.remove(contactPubKeyHex);
|
_cache.remove(contactPubKeyHex);
|
||||||
_cacheAccessOrder.remove(contactPubKeyHex);
|
_cacheAccessOrder.remove(contactPubKeyHex);
|
||||||
_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 +444,7 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await _storage.savePathHistory(contactPubKeyHex, _cache[contactPubKeyHex]!);
|
await _storage.savePathHistory(contactPubKeyHex, _cache[contactPubKeyHex]!);
|
||||||
|
_version++;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,26 +465,81 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
|
|
||||||
final ranked = List<PathRecord>.from(history.recentPaths)
|
final ranked = List<PathRecord>.from(history.recentPaths)
|
||||||
..removeWhere((p) => p.pathBytes.isEmpty);
|
..removeWhere((p) => p.pathBytes.isEmpty);
|
||||||
|
final fastestTripMs = _getFastestKnownTripMs(ranked);
|
||||||
|
final highestRouteWeight = _getHighestKnownRouteWeight(ranked);
|
||||||
|
|
||||||
ranked.sort((a, b) {
|
ranked.sort((a, b) {
|
||||||
final aRate =
|
final scoreCompare =
|
||||||
(a.successCount + 1) / (a.successCount + a.failureCount + 2);
|
_scorePathRecord(
|
||||||
final bRate =
|
b,
|
||||||
(b.successCount + 1) / (b.successCount + b.failureCount + 2);
|
fastestTripMs: fastestTripMs,
|
||||||
if (aRate != bRate) return bRate.compareTo(aRate);
|
highestRouteWeight: highestRouteWeight,
|
||||||
if (a.successCount != b.successCount) {
|
).compareTo(
|
||||||
return b.successCount.compareTo(a.successCount);
|
_scorePathRecord(
|
||||||
|
a,
|
||||||
|
fastestTripMs: fastestTripMs,
|
||||||
|
highestRouteWeight: highestRouteWeight,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (scoreCompare != 0) {
|
||||||
|
return scoreCompare;
|
||||||
|
}
|
||||||
|
if (a.routeWeight != b.routeWeight) {
|
||||||
|
return b.routeWeight.compareTo(a.routeWeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
final aTrip = a.tripTimeMs == 0 ? 999999 : a.tripTimeMs;
|
final aTrip = a.tripTimeMs == 0 ? 999999 : a.tripTimeMs;
|
||||||
final bTrip = b.tripTimeMs == 0 ? 999999 : b.tripTimeMs;
|
final bTrip = b.tripTimeMs == 0 ? 999999 : b.tripTimeMs;
|
||||||
if (aTrip != bTrip) return aTrip.compareTo(bTrip);
|
if (aTrip != bTrip) return aTrip.compareTo(bTrip);
|
||||||
return b.timestamp.compareTo(a.timestamp);
|
final aTime = a.timestamp ?? DateTime.fromMillisecondsSinceEpoch(0);
|
||||||
|
final bTime = b.timestamp ?? DateTime.fromMillisecondsSinceEpoch(0);
|
||||||
|
return bTime.compareTo(aTime);
|
||||||
});
|
});
|
||||||
|
|
||||||
return ranked;
|
return ranked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int? _getFastestKnownTripMs(List<PathRecord> paths) {
|
||||||
|
final knownTrips = paths
|
||||||
|
.where((path) => path.tripTimeMs > 0)
|
||||||
|
.map((path) => path.tripTimeMs)
|
||||||
|
.toList();
|
||||||
|
if (knownTrips.isEmpty) return null;
|
||||||
|
return knownTrips.reduce((a, b) => a < b ? a : b);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _getHighestKnownRouteWeight(List<PathRecord> paths) {
|
||||||
|
if (paths.isEmpty) return 1.0;
|
||||||
|
final highestWeight = paths
|
||||||
|
.map((path) => path.routeWeight)
|
||||||
|
.reduce((a, b) => a > b ? a : b);
|
||||||
|
return highestWeight <= 0 ? 1.0 : highestWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
double _scorePathRecord(
|
||||||
|
PathRecord path, {
|
||||||
|
required int? fastestTripMs,
|
||||||
|
required double highestRouteWeight,
|
||||||
|
}) {
|
||||||
|
final totalAttempts = path.successCount + path.failureCount;
|
||||||
|
final reliability = (path.successCount + 1) / (totalAttempts + 2);
|
||||||
|
final latency = fastestTripMs == null || path.tripTimeMs <= 0
|
||||||
|
? 0.6
|
||||||
|
: (fastestTripMs / path.tripTimeMs).clamp(0.0, 1.0);
|
||||||
|
final freshness = path.timestamp == null
|
||||||
|
? 0.0
|
||||||
|
: 1.0 /
|
||||||
|
(1.0 +
|
||||||
|
(DateTime.now().difference(path.timestamp!).inMinutes /
|
||||||
|
60.0 /
|
||||||
|
24.0));
|
||||||
|
final routeWeight = (path.routeWeight / highestRouteWeight).clamp(0.0, 1.0);
|
||||||
|
|
||||||
|
return (reliability * 0.45) +
|
||||||
|
(latency * 0.25) +
|
||||||
|
(freshness * 0.1) +
|
||||||
|
(routeWeight * 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
bool _pathsEqual(List<int> a, List<int> b) {
|
bool _pathsEqual(List<int> a, List<int> b) {
|
||||||
return listEquals(a, b);
|
return listEquals(a, b);
|
||||||
}
|
}
|
||||||
@@ -360,6 +565,38 @@ class PathHistoryService extends ChangeNotifier {
|
|||||||
_floodStats.remove(oldest);
|
_floodStats.remove(oldest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clearAllHistories() {
|
||||||
|
_cache.clear();
|
||||||
|
_cacheAccessOrder.clear();
|
||||||
|
_autoRotationIndex.clear();
|
||||||
|
_floodStats.clear();
|
||||||
|
_storage.clearAllPathHistories();
|
||||||
|
_version = 0;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DeferredPathRecord {
|
||||||
|
final int hopCount;
|
||||||
|
final int tripTimeMs;
|
||||||
|
final bool wasFloodDiscovery;
|
||||||
|
final List<int> pathBytes;
|
||||||
|
final int successCount;
|
||||||
|
final int failureCount;
|
||||||
|
final double routeWeight;
|
||||||
|
final DateTime? timestamp;
|
||||||
|
|
||||||
|
_DeferredPathRecord({
|
||||||
|
required this.hopCount,
|
||||||
|
required this.tripTimeMs,
|
||||||
|
required this.wasFloodDiscovery,
|
||||||
|
required this.pathBytes,
|
||||||
|
required this.successCount,
|
||||||
|
required this.failureCount,
|
||||||
|
this.routeWeight = 1.0,
|
||||||
|
this.timestamp,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FloodStats {
|
class _FloodStats {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import '../models/delivery_observation.dart';
|
||||||
import '../models/path_history.dart';
|
import '../models/path_history.dart';
|
||||||
import '../storage/prefs_manager.dart';
|
import '../storage/prefs_manager.dart';
|
||||||
|
|
||||||
@@ -6,6 +7,7 @@ class StorageService {
|
|||||||
static const String _pathHistoryPrefix = 'path_history_';
|
static const String _pathHistoryPrefix = 'path_history_';
|
||||||
static const String _pendingMessagesKey = 'pending_messages';
|
static const String _pendingMessagesKey = 'pending_messages';
|
||||||
static const String _repeaterPasswordsKey = 'repeater_passwords';
|
static const String _repeaterPasswordsKey = 'repeater_passwords';
|
||||||
|
static const String _deliveryObservationsKey = 'delivery_observations';
|
||||||
|
|
||||||
Future<void> savePathHistory(
|
Future<void> savePathHistory(
|
||||||
String contactPubKeyHex,
|
String contactPubKeyHex,
|
||||||
@@ -122,4 +124,33 @@ class StorageService {
|
|||||||
final prefs = PrefsManager.instance;
|
final prefs = PrefsManager.instance;
|
||||||
await prefs.remove(_repeaterPasswordsKey);
|
await prefs.remove(_repeaterPasswordsKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> saveDeliveryObservations(
|
||||||
|
List<DeliveryObservation> observations,
|
||||||
|
) async {
|
||||||
|
final prefs = PrefsManager.instance;
|
||||||
|
final jsonStr = jsonEncode(observations.map((o) => o.toJson()).toList());
|
||||||
|
await prefs.setString(_deliveryObservationsKey, jsonStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<DeliveryObservation>> loadDeliveryObservations() async {
|
||||||
|
final prefs = PrefsManager.instance;
|
||||||
|
final jsonStr = prefs.getString(_deliveryObservationsKey);
|
||||||
|
|
||||||
|
if (jsonStr == null) return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
final list = jsonDecode(jsonStr) as List;
|
||||||
|
return list
|
||||||
|
.map((e) => DeliveryObservation.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearDeliveryObservations() async {
|
||||||
|
final prefs = PrefsManager.instance;
|
||||||
|
await prefs.remove(_deliveryObservationsKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user