mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-08-05 15:32:58 +10:00
Compare commits
370 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bdd7fc0cdd | |||
| 5ea044af10 | |||
| 9d20be1c06 | |||
| 9436c2d45a | |||
| 17e55e96bb | |||
| e4cfbb57b4 | |||
| d9f9ff58b4 | |||
| a059f1be45 | |||
| 9e46f8b44c | |||
| a934781009 | |||
| 5fe6738f25 | |||
| c1bcf261d7 | |||
| b570539a2d | |||
| a14833494e | |||
| 457b44de3a | |||
| 36d4a10396 | |||
| 77566b0fe1 | |||
| 10b63e0df2 | |||
| ba6d751346 | |||
| 96d222a580 | |||
| 01ad8471cc | |||
| 2b826757cb | |||
| 9bf649e2c6 | |||
| c7a2bf9a95 | |||
| 82adbd761b | |||
| 9a8bdf00dc | |||
| 8b30342113 | |||
| 817c60a155 | |||
| f08e86cf97 | |||
| a6bb9490a1 | |||
| d1e45fc2ba | |||
| 32fa96431e | |||
| 1e9508d401 | |||
| 36697c6e61 | |||
| c9145c99d3 | |||
| 6b6d9caeeb | |||
| d0e3767db6 | |||
| f9cb0c80a5 | |||
| a26d14bd46 | |||
| 411cd3f8d2 | |||
| 38f4de80b6 | |||
| 7de07c023f | |||
| c272c60f9a | |||
| eca78453d6 | |||
| 3754cf14ea | |||
| 834850fb51 | |||
| e7e2bb91b8 | |||
| 4c492f69ef | |||
| 50f2a8b439 | |||
| 2c8a15538e | |||
| 68eeefa04e | |||
| ebbc367fec | |||
| 2da8995d0b | |||
| 1c376b0056 | |||
| da70d5fc08 | |||
| f63bc4b787 | |||
| 9b1f1e1994 | |||
| 5f475fce4d | |||
| 0228c38621 | |||
| fc7283f076 | |||
| 7eff1df6e2 | |||
| 58252b8a40 | |||
| 630606acdc | |||
| bd030153c1 | |||
| 5140ff383d | |||
| dc57f9b9c0 | |||
| 53cd3f4461 | |||
| 35e296f1cd | |||
| 532401cc94 | |||
| 5321974cbb | |||
| 7c16dde989 | |||
| 9a75c912af | |||
| 767dc1164e | |||
| 14f3429eb5 | |||
| e49e80d330 | |||
| d07372c7e0 | |||
| 990f2bd33d | |||
| 29660d520e | |||
| dbefb0b5f4 | |||
| 4f609f160f | |||
| e313bea3fc | |||
| 77be2b8e6f | |||
| c81c3efe7c | |||
| cac0cc15eb | |||
| 1392c2d00f | |||
| cb63b48b78 | |||
| 4ad4a93a20 | |||
| 4962a48e64 | |||
| b88e5e647a | |||
| 87d11c2e6b | |||
| 7b3c099736 | |||
| 11cb14a925 | |||
| d2df2b0bed | |||
| 723bf7293c | |||
| 53caec3e14 | |||
| 3c440ca3d4 | |||
| 8797d8ffde | |||
| faba120823 | |||
| be690c8194 | |||
| 0ef2194fb0 | |||
| 3664ae34cd | |||
| 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 | |||
| bdb1eb6b42 | |||
| f2ccec2926 | |||
| 31671958d5 | |||
| ea379ce50b | |||
| 50af2e0bc9 | |||
| d5ac84430c | |||
| 190fd3b353 | |||
| a2d1cb2a99 | |||
| 83386a8cde | |||
| acc0fff2dc | |||
| a26055c93f | |||
| 5a70ed48cf | |||
| a777236cd9 | |||
| a42cf77a70 | |||
| 31db565ebf | |||
| 515b9c1f29 | |||
| ea1d728d4f | |||
| 86bde1d178 | |||
| de63733bb9 | |||
| c880c2d107 | |||
| 2a7cc28a3a | |||
| 8a16024642 | |||
| 0f17e2382c | |||
| 6065059241 | |||
| faefef14ff | |||
| ddc87f3a27 | |||
| 2188b49726 | |||
| 1a9b7b0d55 | |||
| 74e29a6c0f | |||
| 7740698cde | |||
| 972ae809e3 | |||
| deb46553f3 | |||
| 58fc55df13 | |||
| ea2f35ec2e | |||
| e2585c0992 | |||
| 78f1a7b28e | |||
| 0121b5f649 | |||
| ec14870aed | |||
| c0516a475d | |||
| b998186430 | |||
| 16b2c24983 | |||
| c8ff0cc943 | |||
| 64bf307d09 | |||
| 88f8066ed3 | |||
| c8f93f9902 | |||
| c34be44950 | |||
| bf5fadd15e | |||
| 3730b2a6c2 | |||
| 173fdf7168 | |||
| 549fc62632 | |||
| 53d073d8f2 | |||
| 7465e81996 | |||
| 677b25908a | |||
| fc55fb98ce | |||
| 2bdd9d35cc | |||
| 1f816f7e08 | |||
| bd27c90216 | |||
| 9bcb8b9ca6 | |||
| aaf79c90c9 | |||
| 08edd2696e | |||
| 0f2d18d6fa | |||
| 298951f8bc | |||
| f3db63ceea | |||
| 47044ae14e | |||
| f4dd76a459 | |||
| ab76a52d71 | |||
| 332bb5ef3a | |||
| 81a423d096 | |||
| 700e85b13d | |||
| 9a27953a6e | |||
| abde4a5e46 | |||
| 6e1cb0482f | |||
| c28b38a233 | |||
| 722caf774e | |||
| 4975b5366e | |||
| d269e181c3 | |||
| 35498c1b90 | |||
| bf4f52a4e3 | |||
| c284e571b0 | |||
| a1ee0789a6 | |||
| 3ca53e967c | |||
| 096e0a4184 | |||
| 40ac95e8e6 | |||
| 377f1df445 | |||
| 9865a03c53 | |||
| a5555bd606 | |||
| 1b4d31a36e | |||
| 8e07440114 | |||
| 71129bdf4d | |||
| ab05cf8b3e | |||
| 452e5337f0 | |||
| 6ac987e7cf | |||
| 5522f9a236 | |||
| b4f79c1aae | |||
| b08defcff4 | |||
| 5676cbd84e | |||
| cf8f01128b | |||
| b5e47ce44f | |||
| 7b2f75047c | |||
| 6d63e49938 | |||
| c7b33f1d1b | |||
| 7288f11c88 | |||
| 2306269384 | |||
| 41ff2353a4 | |||
| b3ad54f296 | |||
| 7cb4c5a334 | |||
| bb8ad70cb9 | |||
| 8fe4129204 | |||
| 2feff809ff | |||
| 51d70ce086 | |||
| b05b62eeee | |||
| 061b715694 | |||
| f38b8b0319 | |||
| 304c389669 | |||
| 7acfe47fd7 | |||
| f4b18d97a1 | |||
| d2b693e5ce | |||
| ba2763a3f6 | |||
| 0c4910e149 | |||
| 4bf2519559 | |||
| 19edeab9d5 | |||
| 0e81d75cce | |||
| 9437846127 | |||
| 50ab46ed40 | |||
| dc193be8ed | |||
| 8a804a3706 | |||
| 1dc90d0e89 | |||
| 5f2312e086 | |||
| a9fbf8c7f5 |
@@ -0,0 +1,38 @@
|
|||||||
|
name: Deploy to Cloudflare Workers
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: 'stable'
|
||||||
|
# Match local development version which provides Dart 3.11.0
|
||||||
|
flutter-version: '3.41.2'
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
|
||||||
|
- name: Get dependencies
|
||||||
|
run: flutter pub get
|
||||||
|
|
||||||
|
- name: Build Web
|
||||||
|
run: bun run build
|
||||||
|
|
||||||
|
- name: Deploy to Cloudflare
|
||||||
|
uses: cloudflare/wrangler-action@v3
|
||||||
|
with:
|
||||||
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
command: deploy
|
||||||
@@ -30,8 +30,12 @@ migrate_working_dir/
|
|||||||
.flutter-plugins-dependencies
|
.flutter-plugins-dependencies
|
||||||
.pub-cache/
|
.pub-cache/
|
||||||
.pub/
|
.pub/
|
||||||
|
pubspec.lock
|
||||||
/build/
|
/build/
|
||||||
/coverage/
|
/coverage/
|
||||||
|
# fvm project files
|
||||||
|
.fvm/
|
||||||
|
.fvmrc
|
||||||
|
|
||||||
# Symbolication related
|
# Symbolication related
|
||||||
app.*.symbols
|
app.*.symbols
|
||||||
@@ -57,6 +61,7 @@ secrets.dart
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.AppleDouble
|
.AppleDouble
|
||||||
.LSOverride
|
.LSOverride
|
||||||
|
macos/Flutter/GeneratedPluginRegistrant.swift
|
||||||
|
|
||||||
# iOS
|
# iOS
|
||||||
**/ios/Pods/
|
**/ios/Pods/
|
||||||
@@ -82,3 +87,6 @@ keystore.properties
|
|||||||
# IDE
|
# IDE
|
||||||
.vscode/launch.json
|
.vscode/launch.json
|
||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
|
|
||||||
|
# Cloudflare Wrangler
|
||||||
|
.wrangler
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
4.0.0
|
||||||
@@ -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,9 +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 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| USB companion | ✅ | 🚧 | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| TCP companion | ✅ | 🚧 | ✅ | ✅ | ✅ | ❌<br>(requires websocket bridge) |
|
||||||
|
| Core Functionality | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Mesh Network | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Map & Location | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Device Management | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Repeater Hub | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
@@ -188,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
|
||||||
@@ -230,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,16 +16,16 @@ 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_11
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
isCoreLibraryDesugaringEnabled = true
|
isCoreLibraryDesugaringEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
<!-- Camera permission for QR code scanning -->
|
<!-- Camera permission for QR code scanning -->
|
||||||
<uses-permission android:name="android.permission.CAMERA"/>
|
<uses-permission android:name="android.permission.CAMERA"/>
|
||||||
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
||||||
|
<uses-feature android:name="android.hardware.usb.host" android:required="false"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="meshcore_open"
|
android:label="meshcore_open"
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
package com.meshcore.meshcore_open
|
package com.meshcore.meshcore_open
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
|
||||||
class MainActivity : FlutterActivity()
|
class MainActivity : FlutterActivity() {
|
||||||
|
private val usbFunctions by lazy { MeshcoreUsbFunctions(this) }
|
||||||
|
|
||||||
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
|
super.configureFlutterEngine(flutterEngine)
|
||||||
|
usbFunctions.configureFlutterEngine(flutterEngine)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
usbFunctions.dispose()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,582 @@
|
|||||||
|
package com.meshcore.meshcore_open
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.hardware.usb.UsbConstants
|
||||||
|
import android.hardware.usb.UsbDevice
|
||||||
|
import android.hardware.usb.UsbDeviceConnection
|
||||||
|
import android.hardware.usb.UsbEndpoint
|
||||||
|
import android.hardware.usb.UsbInterface
|
||||||
|
import android.hardware.usb.UsbManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
import io.flutter.plugin.common.EventChannel
|
||||||
|
import io.flutter.plugin.common.MethodCall
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.ExecutorService
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
class MeshcoreUsbFunctions(
|
||||||
|
private val activity: FlutterActivity,
|
||||||
|
) {
|
||||||
|
private companion object {
|
||||||
|
const val usbRecipientInterface = 0x01
|
||||||
|
}
|
||||||
|
|
||||||
|
private val usbMethodChannelName = "meshcore_open/android_usb_serial"
|
||||||
|
private val usbEventChannelName = "meshcore_open/android_usb_serial_events"
|
||||||
|
private val usbPermissionAction = "com.meshcore.meshcore_open.USB_PERMISSION"
|
||||||
|
|
||||||
|
private val usbManager by lazy {
|
||||||
|
activity.getSystemService(Context.USB_SERVICE) as UsbManager
|
||||||
|
}
|
||||||
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
|
private val usbIoExecutor: ExecutorService = Executors.newSingleThreadExecutor()
|
||||||
|
|
||||||
|
@Volatile private var eventSink: EventChannel.EventSink? = null
|
||||||
|
@Volatile private var usbConnection: UsbDeviceConnection? = null
|
||||||
|
@Volatile private var usbInEndpoint: UsbEndpoint? = null
|
||||||
|
@Volatile private var usbOutEndpoint: UsbEndpoint? = null
|
||||||
|
@Volatile private var controlInterface: UsbInterface? = null
|
||||||
|
@Volatile private var dataInterface: UsbInterface? = null
|
||||||
|
private var readThread: Thread? = null
|
||||||
|
@Volatile private var isReading = false
|
||||||
|
@Volatile private var connectedDeviceName: String? = null
|
||||||
|
|
||||||
|
private var pendingConnectResult: MethodChannel.Result? = null
|
||||||
|
private var pendingConnectPortName: String? = null
|
||||||
|
private var pendingConnectBaudRate: Int = 115200
|
||||||
|
|
||||||
|
private data class PortConfig(
|
||||||
|
val controlInterface: UsbInterface?,
|
||||||
|
val dataInterface: UsbInterface,
|
||||||
|
val inEndpoint: UsbEndpoint,
|
||||||
|
val outEndpoint: UsbEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val permissionReceiver =
|
||||||
|
object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
when (intent?.action) {
|
||||||
|
UsbManager.ACTION_USB_DEVICE_DETACHED -> {
|
||||||
|
handleUsbDetached(intent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
usbPermissionAction -> Unit
|
||||||
|
else -> return
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = pendingConnectResult
|
||||||
|
val portName = pendingConnectPortName
|
||||||
|
pendingConnectResult = null
|
||||||
|
pendingConnectPortName = null
|
||||||
|
|
||||||
|
if (result == null || portName == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val device = findUsbDevice(portName)
|
||||||
|
if (device == null) {
|
||||||
|
result.error(
|
||||||
|
"usb_device_missing",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val granted =
|
||||||
|
intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
|
||||||
|
if (!granted || !usbManager.hasPermission(device)) {
|
||||||
|
result.error("usb_permission_denied", null, null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
openUsbDevice(device, pendingConnectBaudRate, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
|
registerUsbPermissionReceiver()
|
||||||
|
|
||||||
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, usbMethodChannelName)
|
||||||
|
.setMethodCallHandler { call, result ->
|
||||||
|
when (call.method) {
|
||||||
|
"listPorts" -> result.success(listUsbPorts())
|
||||||
|
"connect" -> handleUsbConnect(call, result)
|
||||||
|
"write" -> handleUsbWrite(call, result)
|
||||||
|
"disconnect" -> {
|
||||||
|
scheduleCloseUsbConnection {
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> result.notImplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EventChannel(flutterEngine.dartExecutor.binaryMessenger, usbEventChannelName)
|
||||||
|
.setStreamHandler(
|
||||||
|
object : EventChannel.StreamHandler {
|
||||||
|
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
|
||||||
|
eventSink = events
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCancel(arguments: Any?) {
|
||||||
|
eventSink = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dispose() {
|
||||||
|
closeUsbConnection()
|
||||||
|
usbIoExecutor.shutdownNow()
|
||||||
|
try {
|
||||||
|
activity.unregisterReceiver(permissionReceiver)
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun registerUsbPermissionReceiver() {
|
||||||
|
val filter =
|
||||||
|
IntentFilter().apply {
|
||||||
|
addAction(usbPermissionAction)
|
||||||
|
addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
activity.registerReceiver(permissionReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
activity.registerReceiver(permissionReceiver, filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun listUsbPorts(): List<String> {
|
||||||
|
return usbManager.deviceList.values.map { device ->
|
||||||
|
val productName = device.productName ?: "USB Serial Device"
|
||||||
|
val vendorProduct =
|
||||||
|
String.format(
|
||||||
|
Locale.US,
|
||||||
|
"VID:%04X PID:%04X",
|
||||||
|
device.vendorId,
|
||||||
|
device.productId,
|
||||||
|
)
|
||||||
|
"${device.deviceName} - $productName - $vendorProduct"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUsbConnect(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val portName = call.argument<String>("portName")
|
||||||
|
val baudRate = call.argument<Int>("baudRate") ?: 115200
|
||||||
|
if (portName.isNullOrBlank()) {
|
||||||
|
result.error("usb_invalid_port", null, null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val device = findUsbDevice(portName)
|
||||||
|
if (device == null) {
|
||||||
|
result.error("usb_device_missing", null, null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usbManager.hasPermission(device)) {
|
||||||
|
openUsbDevice(device, baudRate, result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingConnectResult != null) {
|
||||||
|
result.error("usb_busy", null, null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingConnectResult = result
|
||||||
|
pendingConnectPortName = portName
|
||||||
|
pendingConnectBaudRate = baudRate
|
||||||
|
|
||||||
|
val permissionIntent = PendingIntent.getBroadcast(
|
||||||
|
activity,
|
||||||
|
0,
|
||||||
|
Intent(usbPermissionAction).setPackage(activity.packageName),
|
||||||
|
pendingIntentFlags(),
|
||||||
|
)
|
||||||
|
usbManager.requestPermission(device, permissionIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUsbWrite(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val data = call.argument<ByteArray>("data")
|
||||||
|
val connection = usbConnection
|
||||||
|
val endpoint = usbOutEndpoint
|
||||||
|
if (data == null) {
|
||||||
|
result.error("usb_invalid_data", null, null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (connection == null || endpoint == null) {
|
||||||
|
result.error("usb_not_connected", null, null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
usbIoExecutor.execute {
|
||||||
|
try {
|
||||||
|
writeToDevice(data)
|
||||||
|
mainHandler.post { result.success(null) }
|
||||||
|
} catch (error: Exception) {
|
||||||
|
mainHandler.post {
|
||||||
|
result.error("usb_write_failed", error.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findUsbDevice(portName: String): UsbDevice? {
|
||||||
|
val devices = usbManager.deviceList.values
|
||||||
|
val exactMatch = devices.firstOrNull { it.deviceName == portName }
|
||||||
|
if (exactMatch != null) {
|
||||||
|
return exactMatch
|
||||||
|
}
|
||||||
|
|
||||||
|
val normalizedName = portName.substringBefore(" - ").trim()
|
||||||
|
return devices.firstOrNull { it.deviceName == normalizedName }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openUsbDevice(
|
||||||
|
device: UsbDevice,
|
||||||
|
baudRate: Int,
|
||||||
|
result: MethodChannel.Result,
|
||||||
|
) {
|
||||||
|
usbIoExecutor.execute {
|
||||||
|
try {
|
||||||
|
closeUsbConnection()
|
||||||
|
|
||||||
|
val config = resolvePortConfig(device)
|
||||||
|
if (config == null) {
|
||||||
|
mainHandler.post {
|
||||||
|
result.error(
|
||||||
|
"usb_driver_missing",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return@execute
|
||||||
|
}
|
||||||
|
|
||||||
|
val connection = usbManager.openDevice(device)
|
||||||
|
if (connection == null) {
|
||||||
|
mainHandler.post {
|
||||||
|
result.error(
|
||||||
|
"usb_open_failed",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return@execute
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!connection.claimInterface(config.dataInterface, true)) {
|
||||||
|
connection.close()
|
||||||
|
mainHandler.post {
|
||||||
|
result.error(
|
||||||
|
"usb_open_failed",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return@execute
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.controlInterface != null &&
|
||||||
|
config.controlInterface.id != config.dataInterface.id &&
|
||||||
|
!connection.claimInterface(config.controlInterface, true)
|
||||||
|
) {
|
||||||
|
connection.releaseInterface(config.dataInterface)
|
||||||
|
connection.close()
|
||||||
|
mainHandler.post {
|
||||||
|
result.error(
|
||||||
|
"usb_open_failed",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return@execute
|
||||||
|
}
|
||||||
|
|
||||||
|
usbConnection = connection
|
||||||
|
usbInEndpoint = config.inEndpoint
|
||||||
|
usbOutEndpoint = config.outEndpoint
|
||||||
|
controlInterface = config.controlInterface
|
||||||
|
dataInterface = config.dataInterface
|
||||||
|
|
||||||
|
configureDevice(connection, config, baudRate)
|
||||||
|
|
||||||
|
connectedDeviceName = device.deviceName
|
||||||
|
startReadLoop()
|
||||||
|
|
||||||
|
mainHandler.post {
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
|
} catch (error: Exception) {
|
||||||
|
closeUsbConnection()
|
||||||
|
mainHandler.post {
|
||||||
|
result.error("usb_connect_failed", error.message, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolvePortConfig(device: UsbDevice): PortConfig? {
|
||||||
|
var preferredDataInterface: UsbInterface? = null
|
||||||
|
var preferredInEndpoint: UsbEndpoint? = null
|
||||||
|
var preferredOutEndpoint: UsbEndpoint? = null
|
||||||
|
var fallbackDataInterface: UsbInterface? = null
|
||||||
|
var fallbackInEndpoint: UsbEndpoint? = null
|
||||||
|
var fallbackOutEndpoint: UsbEndpoint? = null
|
||||||
|
var preferredControlInterface: UsbInterface? = null
|
||||||
|
|
||||||
|
for (interfaceIndex in 0 until device.interfaceCount) {
|
||||||
|
val usbInterface = device.getInterface(interfaceIndex)
|
||||||
|
var inEndpoint: UsbEndpoint? = null
|
||||||
|
var outEndpoint: UsbEndpoint? = null
|
||||||
|
|
||||||
|
for (endpointIndex in 0 until usbInterface.endpointCount) {
|
||||||
|
val endpoint = usbInterface.getEndpoint(endpointIndex)
|
||||||
|
if (endpoint.type != UsbConstants.USB_ENDPOINT_XFER_BULK) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
when (endpoint.direction) {
|
||||||
|
UsbConstants.USB_DIR_IN -> if (inEndpoint == null) inEndpoint = endpoint
|
||||||
|
UsbConstants.USB_DIR_OUT -> if (outEndpoint == null) outEndpoint = endpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasDataPair = inEndpoint != null && outEndpoint != null
|
||||||
|
when {
|
||||||
|
usbInterface.interfaceClass == UsbConstants.USB_CLASS_COMM &&
|
||||||
|
preferredControlInterface == null -> {
|
||||||
|
preferredControlInterface = usbInterface
|
||||||
|
}
|
||||||
|
hasDataPair &&
|
||||||
|
usbInterface.interfaceClass == UsbConstants.USB_CLASS_CDC_DATA -> {
|
||||||
|
preferredDataInterface = usbInterface
|
||||||
|
preferredInEndpoint = inEndpoint
|
||||||
|
preferredOutEndpoint = outEndpoint
|
||||||
|
}
|
||||||
|
hasDataPair && fallbackDataInterface == null -> {
|
||||||
|
fallbackDataInterface = usbInterface
|
||||||
|
fallbackInEndpoint = inEndpoint
|
||||||
|
fallbackOutEndpoint = outEndpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val dataInterface = preferredDataInterface ?: fallbackDataInterface ?: return null
|
||||||
|
val inEndpoint = preferredInEndpoint ?: fallbackInEndpoint ?: return null
|
||||||
|
val outEndpoint = preferredOutEndpoint ?: fallbackOutEndpoint ?: return null
|
||||||
|
return PortConfig(preferredControlInterface, dataInterface, inEndpoint, outEndpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun configureDevice(
|
||||||
|
connection: UsbDeviceConnection,
|
||||||
|
config: PortConfig,
|
||||||
|
baudRate: Int,
|
||||||
|
) {
|
||||||
|
val control = config.controlInterface ?: return
|
||||||
|
val lineCoding =
|
||||||
|
byteArrayOf(
|
||||||
|
(baudRate and 0xFF).toByte(),
|
||||||
|
((baudRate shr 8) and 0xFF).toByte(),
|
||||||
|
((baudRate shr 16) and 0xFF).toByte(),
|
||||||
|
((baudRate shr 24) and 0xFF).toByte(),
|
||||||
|
0, // stop bits: 1
|
||||||
|
0, // parity: none
|
||||||
|
8, // data bits
|
||||||
|
)
|
||||||
|
|
||||||
|
val lineCodingResult =
|
||||||
|
connection.controlTransfer(
|
||||||
|
UsbConstants.USB_DIR_OUT or
|
||||||
|
UsbConstants.USB_TYPE_CLASS or
|
||||||
|
usbRecipientInterface,
|
||||||
|
0x20,
|
||||||
|
0,
|
||||||
|
control.id,
|
||||||
|
lineCoding,
|
||||||
|
lineCoding.size,
|
||||||
|
1000,
|
||||||
|
)
|
||||||
|
if (lineCodingResult < 0) {
|
||||||
|
throw IllegalStateException("Failed to configure USB line coding")
|
||||||
|
}
|
||||||
|
|
||||||
|
val controlLineResult =
|
||||||
|
connection.controlTransfer(
|
||||||
|
UsbConstants.USB_DIR_OUT or
|
||||||
|
UsbConstants.USB_TYPE_CLASS or
|
||||||
|
usbRecipientInterface,
|
||||||
|
0x22,
|
||||||
|
0x0001, // DTR on, RTS off
|
||||||
|
control.id,
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
1000,
|
||||||
|
)
|
||||||
|
if (controlLineResult < 0) {
|
||||||
|
throw IllegalStateException("Failed to configure USB control line state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startReadLoop() {
|
||||||
|
val connection = usbConnection ?: return
|
||||||
|
val endpoint = usbInEndpoint ?: return
|
||||||
|
|
||||||
|
isReading = true
|
||||||
|
readThread =
|
||||||
|
Thread({
|
||||||
|
val packetSize = endpoint.maxPacketSize.coerceAtLeast(64)
|
||||||
|
val buffer = ByteArray(packetSize * 4)
|
||||||
|
try {
|
||||||
|
while (isReading) {
|
||||||
|
val bytesRead = connection.bulkTransfer(endpoint, buffer, buffer.size, 250)
|
||||||
|
if (!isReading) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (bytesRead <= 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val packet = buffer.copyOf(bytesRead)
|
||||||
|
mainHandler.post {
|
||||||
|
eventSink?.success(packet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: Exception) {
|
||||||
|
if (isReading) {
|
||||||
|
mainHandler.post {
|
||||||
|
eventSink?.error(
|
||||||
|
"usb_io_error",
|
||||||
|
error.message ?: "USB serial I/O error",
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
scheduleCloseUsbConnection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, "MeshCoreUsbRead").also { thread ->
|
||||||
|
thread.isDaemon = true
|
||||||
|
thread.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun writeToDevice(data: ByteArray) {
|
||||||
|
val connection = usbConnection ?: throw IllegalStateException("USB connection missing")
|
||||||
|
val endpoint = usbOutEndpoint ?: throw IllegalStateException("USB output endpoint missing")
|
||||||
|
var offset = 0
|
||||||
|
val maxPacketSize = endpoint.maxPacketSize.coerceAtLeast(64)
|
||||||
|
while (offset < data.size) {
|
||||||
|
val chunkSize = minOf(maxPacketSize, data.size - offset)
|
||||||
|
val chunk = data.copyOfRange(offset, offset + chunkSize)
|
||||||
|
val bytesWritten = connection.bulkTransfer(endpoint, chunk, chunkSize, 1000)
|
||||||
|
if (bytesWritten != chunkSize) {
|
||||||
|
throw IllegalStateException("Short USB write: wrote $bytesWritten of $chunkSize bytes")
|
||||||
|
}
|
||||||
|
offset += chunkSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scheduleCloseUsbConnection(onComplete: (() -> Unit)? = null) {
|
||||||
|
usbIoExecutor.execute {
|
||||||
|
closeUsbConnection()
|
||||||
|
if (onComplete != null) {
|
||||||
|
mainHandler.post(onComplete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun closeUsbConnection() {
|
||||||
|
isReading = false
|
||||||
|
readThread?.interrupt()
|
||||||
|
if (readThread != null && readThread !== Thread.currentThread()) {
|
||||||
|
try {
|
||||||
|
readThread?.join(300)
|
||||||
|
} catch (_: InterruptedException) {
|
||||||
|
Thread.currentThread().interrupt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
readThread = null
|
||||||
|
|
||||||
|
val connection = usbConnection
|
||||||
|
val claimedControl = controlInterface
|
||||||
|
val claimedData = dataInterface
|
||||||
|
|
||||||
|
usbInEndpoint = null
|
||||||
|
usbOutEndpoint = null
|
||||||
|
controlInterface = null
|
||||||
|
dataInterface = null
|
||||||
|
usbConnection = null
|
||||||
|
|
||||||
|
if (connection != null) {
|
||||||
|
if (claimedControl != null) {
|
||||||
|
try {
|
||||||
|
connection.releaseInterface(claimedControl)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (claimedData != null && claimedData.id != claimedControl?.id) {
|
||||||
|
try {
|
||||||
|
connection.releaseInterface(claimedData)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
connection.close()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connectedDeviceName = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUsbDetached(intent: Intent) {
|
||||||
|
val detachedDevice =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)
|
||||||
|
}
|
||||||
|
|
||||||
|
val detachedName = detachedDevice?.deviceName ?: return
|
||||||
|
|
||||||
|
if (pendingConnectPortName == detachedName) {
|
||||||
|
pendingConnectResult?.error(
|
||||||
|
"usb_device_detached",
|
||||||
|
"USB device was removed before the connection completed",
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
pendingConnectResult = null
|
||||||
|
pendingConnectPortName = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectedDeviceName == detachedName) {
|
||||||
|
scheduleCloseUsbConnection {
|
||||||
|
eventSink?.error(
|
||||||
|
"usb_device_detached",
|
||||||
|
"USB device was disconnected",
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pendingIntentFlags(): Int {
|
||||||
|
var flags = PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
flags = flags or PendingIntent.FLAG_MUTABLE
|
||||||
|
}
|
||||||
|
return flags
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M268-240 42-466l57-56 170 170 56 56-57 56Zm226 0L268-466l56-57 170 170 368-368 56 57-424 424Zm0-226-57-56 198-198 57 56-198 198Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 253 B |
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
@@ -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.
|
||||||
+3334
-560
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,21 +16,31 @@ 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) {
|
||||||
|
throw RangeError(
|
||||||
|
'Attempted to read $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
|
||||||
|
);
|
||||||
|
}
|
||||||
final data = _buffer.sublist(_pointer, _pointer + count);
|
final data = _buffer.sublist(_pointer, _pointer + count);
|
||||||
_pointer += count;
|
_pointer += count;
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
void skipBytes(int count) {
|
void skipBytes(int count) {
|
||||||
|
_lastPointer = _pointer;
|
||||||
|
if (_pointer + count > _buffer.length) {
|
||||||
|
throw RangeError(
|
||||||
|
'Attempted to skip $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
|
||||||
|
);
|
||||||
|
}
|
||||||
_pointer += count;
|
_pointer += count;
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -41,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() =>
|
||||||
@@ -62,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
|
||||||
@@ -104,23 +139,38 @@ class BufferWriter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void writeHex(String hex) {
|
void writeHex(String hex) {
|
||||||
// Validate hex string length is even and not empty
|
writeBytes(hex2Uint8List(hex));
|
||||||
if (hex.isEmpty || hex.length % 2 != 0) {
|
|
||||||
throw FormatException('Invalid hex string length: ${hex.length}');
|
|
||||||
}
|
|
||||||
List<int> result = [];
|
|
||||||
for (int i = 0; i < hex.length ~/ 2; i++) {
|
|
||||||
final hexByte = hex.substring(i * 2, i * 2 + 2);
|
|
||||||
final byte = int.tryParse(hexByte, radix: 16);
|
|
||||||
if (byte == null) {
|
|
||||||
throw FormatException(
|
|
||||||
'Invalid hex characters at position $i: $hexByte',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
result.add(byte);
|
|
||||||
}
|
|
||||||
writeBytes(Uint8List.fromList(result));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
if (hex.isEmpty || hex.length % 2 != 0) {
|
||||||
|
throw FormatException('Invalid hex string length: ${hex.length}');
|
||||||
|
}
|
||||||
|
List<int> result = [];
|
||||||
|
for (int i = 0; i < hex.length ~/ 2; i++) {
|
||||||
|
final hexByte = hex.substring(i * 2, i * 2 + 2);
|
||||||
|
final byte = int.tryParse(hexByte, radix: 16);
|
||||||
|
if (byte == null) {
|
||||||
|
throw FormatException('Invalid hex characters at position $i: $hexByte');
|
||||||
|
}
|
||||||
|
result.add(byte);
|
||||||
|
}
|
||||||
|
return Uint8List.fromList(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command codes (to device)
|
// Command codes (to device)
|
||||||
@@ -151,22 +201,28 @@ const int cmdGetContactByKey = 30;
|
|||||||
const int cmdGetChannel = 31;
|
const int cmdGetChannel = 31;
|
||||||
const int cmdSetChannel = 32;
|
const int cmdSetChannel = 32;
|
||||||
const int cmdSendTracePath = 36;
|
const int cmdSendTracePath = 36;
|
||||||
const int cmdGetRadioSettings = 57;
|
const int cmdSetOtherParams = 38;
|
||||||
const int 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 cmdGetStats = 56;
|
||||||
|
const int cmdSendAnonReq = 57;
|
||||||
|
const int cmdSetAutoAddConfig = 58;
|
||||||
|
const int cmdGetAutoAddConfig = 59;
|
||||||
|
const int cmdSetPathHashMode = 61;
|
||||||
|
|
||||||
// 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;
|
||||||
const int reqTypeKeepAlive = 0x02;
|
const int reqTypeKeepAlive = 0x02;
|
||||||
const int reqTypeGetTelemetry = 0x03;
|
const int reqTypeGetTelemetry = 0x03;
|
||||||
const int reqTypeGetAccessList = 0x05;
|
const int reqTypeGetAccessList = 0x05;
|
||||||
const int reqTypeGetNeighbours = 0x06;
|
const int reqTypeGetNeighbors = 0x06;
|
||||||
|
|
||||||
// Repeater response codes
|
// Repeater response codes
|
||||||
const int respServerLoginOk = 0;
|
const int respServerLoginOk = 0;
|
||||||
@@ -189,8 +245,13 @@ 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;
|
||||||
|
const int respCodeStats = 24;
|
||||||
|
|
||||||
|
const int statsTypeCore = 0;
|
||||||
|
const int statsTypeRadio = 1;
|
||||||
|
const int statsTypePackets = 2;
|
||||||
|
|
||||||
// Push codes (async from device)
|
// Push codes (async from device)
|
||||||
const int pushCodeAdvert = 0x80;
|
const int pushCodeAdvert = 0x80;
|
||||||
@@ -212,8 +273,49 @@ 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
|
||||||
|
const int payloadTypeREQ =
|
||||||
|
0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
|
||||||
|
const int payloadTypeRESPONSE =
|
||||||
|
0x01; // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
|
||||||
|
const int payloadTypeTXTMSG =
|
||||||
|
0x02; // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text)
|
||||||
|
const int payloadTypeACK = 0x03; // a simple ack
|
||||||
|
const int payloadTypeADVERT = 0x04; // a node advertising its Identity
|
||||||
|
const int payloadTypeGRPTXT =
|
||||||
|
0x05; // an (unverified) group text message (prefixed with channel hash, MAC) (enc data: timestamp, "name: msg")
|
||||||
|
const int payloadTypeGRPDATA =
|
||||||
|
0x06; // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: timestamp, blob)
|
||||||
|
const int payloadTypeANONREQ =
|
||||||
|
0x07; // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...)
|
||||||
|
const int payloadTypePATH =
|
||||||
|
0x08; // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra)
|
||||||
|
const int payloadTypeTRACE = 0x09; // trace a path, collecting SNI for each hop
|
||||||
|
const int payloadTypeMULTIPART = 0x0A; // packet is one of a set of packets
|
||||||
|
const int payloadTypeCONTROL = 0x0B; // a control/discovery packet
|
||||||
|
//...
|
||||||
|
const int payloadTypeRawCustom =
|
||||||
|
0x0F; // custom packet as raw bytes, for applications with custom encryption, payloads, etc
|
||||||
|
|
||||||
|
//auto-add flags
|
||||||
|
const int autoAddOverwriteOldestFlag =
|
||||||
|
1 << 0; // 0x01 - overwrite oldest non-favourite when full
|
||||||
|
const int autoAddChatFlag =
|
||||||
|
1 << 1; // 0x02 - auto-add Chat (Companion) (ADV_TYPE_CHAT)
|
||||||
|
const int autoAddRepeaterFlag =
|
||||||
|
1 << 2; // 0x04 - auto-add Repeater (ADV_TYPE_REPEATER)
|
||||||
|
const int autoAddRoomServerFlag =
|
||||||
|
1 << 3; // 0x08 - auto-add Room Server (ADV_TYPE_ROOM)
|
||||||
|
const int autoAddSensorFlag =
|
||||||
|
1 << 4; // 0x10 - auto-add Sensor (ADV_TYPE_SENSOR)
|
||||||
|
|
||||||
// Sizes
|
// Sizes
|
||||||
const int pubKeySize = 32;
|
const int pubKeySize = 32;
|
||||||
|
const int 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;
|
||||||
@@ -255,13 +357,17 @@ int _minPositive(int a, int b) {
|
|||||||
const int contactPubKeyOffset = 1;
|
const int contactPubKeyOffset = 1;
|
||||||
const int contactTypeOffset = 33;
|
const int contactTypeOffset = 33;
|
||||||
const int contactFlagsOffset = 34;
|
const int contactFlagsOffset = 34;
|
||||||
|
const int contactFlagFavorite = 0x01;
|
||||||
|
const int 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
|
||||||
@@ -273,52 +379,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];
|
|
||||||
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
|
final message = BufferReader(frame);
|
||||||
|
try {
|
||||||
|
final code = message.readByte();
|
||||||
|
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Companion radio layout:
|
||||||
|
// [code][snr?][res?][res?][prefix x6][path_len][txt_type][timestamp x4][extra?][text...]
|
||||||
|
if (code == respCodeContactMsgRecvV3) {
|
||||||
|
// Skip SNR and reserved bytes in v3 layout
|
||||||
|
message.skipBytes(3);
|
||||||
|
}
|
||||||
|
final senderPrefix = message.readBytes(6); // public key
|
||||||
|
message.skipBytes(1); // path length
|
||||||
|
final textType = message.readByte();
|
||||||
|
message.skipBytes(4); // timestamp (4 bytes)
|
||||||
|
|
||||||
|
final shiftedType = textType >> 2;
|
||||||
|
final isSigned = shiftedType == txtTypeSigned || textType == txtTypeSigned;
|
||||||
|
if (isSigned) {
|
||||||
|
// Signed messages have a 4-byte signature after the timestamp, before the text
|
||||||
|
message.skipBytes(4);
|
||||||
|
}
|
||||||
|
final text = message.readCString();
|
||||||
|
if (text.isEmpty) return null;
|
||||||
|
|
||||||
|
return ParsedContactText(senderPrefix: senderPrefix, text: text);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error parsing contact message text: $e');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Companion radio layout:
|
|
||||||
// [code][snr?][res?][res?][prefix x6][path_len][txt_type][timestamp x4][extra?][text...]
|
|
||||||
final isV3 = code == respCodeContactMsgRecvV3;
|
|
||||||
final prefixOffset = isV3 ? 4 : 1;
|
|
||||||
const prefixLen = 6;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
var text = readCString(
|
|
||||||
frame,
|
|
||||||
baseTextOffset,
|
|
||||||
frame.length - baseTextOffset,
|
|
||||||
).trim();
|
|
||||||
if (text.isEmpty && frame.length > baseTextOffset + 4) {
|
|
||||||
text = readCString(
|
|
||||||
frame,
|
|
||||||
baseTextOffset + 4,
|
|
||||||
frame.length - (baseTextOffset + 4),
|
|
||||||
).trim();
|
|
||||||
}
|
|
||||||
if (text.isEmpty) return null;
|
|
||||||
|
|
||||||
final senderPrefix = frame.sublist(prefixOffset, prefixOffset + prefixLen);
|
|
||||||
return ParsedContactText(senderPrefix: senderPrefix, text: text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to read uint32 little-endian
|
// Helper to read uint32 little-endian
|
||||||
@@ -341,18 +439,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
|
||||||
@@ -412,7 +501,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);
|
||||||
@@ -472,6 +561,17 @@ Uint8List buildGetBattAndStorageFrame() {
|
|||||||
return Uint8List.fromList([cmdGetBattAndStorage]);
|
return Uint8List.fromList([cmdGetBattAndStorage]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Companion radio stats: [56][statsType] where statsType is statsTypeCore/Radio/Packets.
|
||||||
|
Uint8List buildGetStatsFrame(int statsType) {
|
||||||
|
return Uint8List.fromList([cmdGetStats, statsType & 0xFF]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Path hash width on air: [61][0][mode], mode 0..2 → (mode+1) bytes per hop hash.
|
||||||
|
Uint8List buildSetPathHashModeFrame(int mode) {
|
||||||
|
final m = mode.clamp(0, 2);
|
||||||
|
return Uint8List.fromList([cmdSetPathHashMode, 0, m]);
|
||||||
|
}
|
||||||
|
|
||||||
// Build CMD_SET_DEVICE_TIME frame
|
// Build CMD_SET_DEVICE_TIME frame
|
||||||
Uint8List buildSetDeviceTimeFrame(int timestamp) {
|
Uint8List buildSetDeviceTimeFrame(int timestamp) {
|
||||||
final writer = BufferWriter();
|
final writer = BufferWriter();
|
||||||
@@ -592,14 +692,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);
|
||||||
@@ -608,17 +711,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);
|
||||||
@@ -627,6 +720,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -639,16 +753,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
|
||||||
@@ -728,7 +841,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);
|
||||||
@@ -773,10 +886,10 @@ Uint8List buildExportContactFrame(Uint8List pubKey) {
|
|||||||
|
|
||||||
// Build a import contact frame
|
// Build a import contact frame
|
||||||
// [cmd][contact_frame x98+]
|
// [cmd][contact_frame x98+]
|
||||||
Uint8List buildImportContactFrame(String contactFrame) {
|
Uint8List buildImportContactFrame(Uint8List contactFrame) {
|
||||||
final writer = BufferWriter();
|
final writer = BufferWriter();
|
||||||
writer.writeByte(cmdImportContact);
|
writer.writeByte(cmdImportContact);
|
||||||
writer.writeHex(contactFrame);
|
writer.writeBytes(contactFrame);
|
||||||
return writer.toBytes();
|
return writer.toBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -788,3 +901,57 @@ Uint8List buildZeroHopContact(Uint8List pubKey) {
|
|||||||
writer.writeBytes(pubKey);
|
writer.writeBytes(pubKey);
|
||||||
return writer.toBytes();
|
return writer.toBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build CMD_SET_OTHER_PARAMS frame
|
||||||
|
// Format: [cmd][allowTelemetryFlags][advertLocationPolicy][multiAcks]
|
||||||
|
Uint8List buildSetOtherParamsFrame(
|
||||||
|
int allowTelemetryFlags,
|
||||||
|
int advertLocationPolicy,
|
||||||
|
int multiAcks,
|
||||||
|
) {
|
||||||
|
final writer = BufferWriter();
|
||||||
|
writer.writeByte(cmdSetOtherParams);
|
||||||
|
//Going forward the app will just set Auto Add Contacts to disabled, and use the filter flags
|
||||||
|
//Allow Auto Add Contacts use inverted logic (0x01 = disabled, 0x00 = enabled).
|
||||||
|
writer.writeByte(0x01);
|
||||||
|
writer.writeByte(allowTelemetryFlags); // Allow Telemetry Flags
|
||||||
|
writer.writeByte(advertLocationPolicy); // Advertisement Location Policy
|
||||||
|
writer.writeByte(multiAcks); // Multi Acknowledgements
|
||||||
|
return writer.toBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build CMD_SET_AUTO_ADD_CONFIG frame
|
||||||
|
// Format: [cmd][flags]
|
||||||
|
Uint8List buildSetAutoAddConfigFrame({
|
||||||
|
required bool autoAddChat,
|
||||||
|
required bool autoAddRepeater,
|
||||||
|
required bool autoAddRoomServer,
|
||||||
|
required bool autoAddSensor,
|
||||||
|
required bool overwriteOldest,
|
||||||
|
}) {
|
||||||
|
final writer = BufferWriter();
|
||||||
|
writer.writeByte(cmdSetAutoAddConfig);
|
||||||
|
int flags = 0;
|
||||||
|
if (autoAddChat) flags |= autoAddChatFlag;
|
||||||
|
if (autoAddRepeater) flags |= autoAddRepeaterFlag;
|
||||||
|
if (autoAddRoomServer) flags |= autoAddRoomServerFlag;
|
||||||
|
if (autoAddSensor) flags |= autoAddSensorFlag;
|
||||||
|
if (overwriteOldest) flags |= autoAddOverwriteOldestFlag;
|
||||||
|
writer.writeByte(flags);
|
||||||
|
return writer.toBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
//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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
class MeshCoreUuids {
|
||||||
|
static const String service = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
|
||||||
|
static const String rxCharacteristic = "6e400002-b5a3-f393-e0a9-e50e24dcca9e";
|
||||||
|
static const String txCharacteristic = "6e400003-b5a3-f393-e0a9-e50e24dcca9e";
|
||||||
|
|
||||||
|
static const List<String> deviceNamePrefixes = [
|
||||||
|
"MeshCore-",
|
||||||
|
"Whisper-",
|
||||||
|
"WisCore-",
|
||||||
|
"HT-",
|
||||||
|
];
|
||||||
|
}
|
||||||
+175
-161
@@ -1,4 +1,6 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
import 'package:meshcore_open/utils/app_logger.dart';
|
||||||
|
|
||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
|
|
||||||
class CayenneLpp {
|
class CayenneLpp {
|
||||||
@@ -84,180 +86,192 @@ class CayenneLpp {
|
|||||||
static List<Map<String, dynamic>> parse(Uint8List bytes) {
|
static List<Map<String, dynamic>> parse(Uint8List bytes) {
|
||||||
final buffer = BufferReader(bytes);
|
final buffer = BufferReader(bytes);
|
||||||
final telemetry = <Map<String, dynamic>>[];
|
final telemetry = <Map<String, dynamic>>[];
|
||||||
|
try {
|
||||||
|
while (buffer.remaining >= 2) {
|
||||||
|
final channel = buffer.readUInt8();
|
||||||
|
final type = buffer.readUInt8();
|
||||||
|
|
||||||
while (buffer.remaining >= 2) {
|
if (channel == 0 && type == 0) {
|
||||||
final channel = buffer.readUInt8();
|
break;
|
||||||
final type = buffer.readUInt8();
|
}
|
||||||
|
|
||||||
if (channel == 0 && type == 0) {
|
switch (type) {
|
||||||
break;
|
case lppGenericSensor:
|
||||||
}
|
telemetry.add({
|
||||||
|
'channel': channel,
|
||||||
switch (type) {
|
'type': type,
|
||||||
case lppGenericSensor:
|
'value': buffer.readUInt32BE(),
|
||||||
telemetry.add({
|
});
|
||||||
'channel': channel,
|
break;
|
||||||
'type': type,
|
case lppLuminosity:
|
||||||
'value': buffer.readUInt32BE(),
|
telemetry.add({
|
||||||
});
|
'channel': channel,
|
||||||
break;
|
'type': type,
|
||||||
case lppLuminosity:
|
'value': buffer.readUInt16BE(),
|
||||||
telemetry.add({
|
});
|
||||||
'channel': channel,
|
break;
|
||||||
'type': type,
|
case lppPresence:
|
||||||
'value': buffer.readUInt16BE(),
|
telemetry.add({
|
||||||
});
|
'channel': channel,
|
||||||
break;
|
'type': type,
|
||||||
case lppPresence:
|
'value': buffer.readUInt8(),
|
||||||
telemetry.add({
|
});
|
||||||
'channel': channel,
|
break;
|
||||||
'type': type,
|
case lppTemperature:
|
||||||
'value': buffer.readUInt8(),
|
telemetry.add({
|
||||||
});
|
'channel': channel,
|
||||||
break;
|
'type': type,
|
||||||
case lppTemperature:
|
'value': buffer.readInt16BE() / 10,
|
||||||
telemetry.add({
|
});
|
||||||
'channel': channel,
|
break;
|
||||||
'type': type,
|
case lppRelativeHumidity:
|
||||||
'value': buffer.readInt16BE() / 10,
|
telemetry.add({
|
||||||
});
|
'channel': channel,
|
||||||
break;
|
'type': type,
|
||||||
case lppRelativeHumidity:
|
'value': buffer.readUInt8() / 2,
|
||||||
telemetry.add({
|
});
|
||||||
'channel': channel,
|
break;
|
||||||
'type': type,
|
case lppBarometricPressure:
|
||||||
'value': buffer.readUInt8() / 2,
|
telemetry.add({
|
||||||
});
|
'channel': channel,
|
||||||
break;
|
'type': type,
|
||||||
case lppBarometricPressure:
|
'value': buffer.readUInt16BE() / 10,
|
||||||
telemetry.add({
|
});
|
||||||
'channel': channel,
|
break;
|
||||||
'type': type,
|
case lppVoltage:
|
||||||
'value': buffer.readUInt16BE() / 10,
|
telemetry.add({
|
||||||
});
|
'channel': channel,
|
||||||
break;
|
'type': type,
|
||||||
case lppVoltage:
|
'value': buffer.readInt16BE() / 100,
|
||||||
telemetry.add({
|
});
|
||||||
'channel': channel,
|
break;
|
||||||
'type': type,
|
case lppCurrent:
|
||||||
'value': buffer.readInt16BE() / 100,
|
telemetry.add({
|
||||||
});
|
'channel': channel,
|
||||||
break;
|
'type': type,
|
||||||
case lppCurrent:
|
'value': buffer.readInt16BE() / 1000,
|
||||||
telemetry.add({
|
});
|
||||||
'channel': channel,
|
break;
|
||||||
'type': type,
|
case lppPercentage:
|
||||||
'value': buffer.readInt16BE() / 1000,
|
telemetry.add({
|
||||||
});
|
'channel': channel,
|
||||||
break;
|
'type': type,
|
||||||
case lppPercentage:
|
'value': buffer.readUInt8(),
|
||||||
telemetry.add({
|
});
|
||||||
'channel': channel,
|
break;
|
||||||
'type': type,
|
case lppConcentration:
|
||||||
'value': buffer.readUInt8(),
|
telemetry.add({
|
||||||
});
|
'channel': channel,
|
||||||
break;
|
'type': type,
|
||||||
case lppConcentration:
|
'value': buffer.readUInt16BE(),
|
||||||
telemetry.add({
|
});
|
||||||
'channel': channel,
|
break;
|
||||||
'type': type,
|
case lppPower:
|
||||||
'value': buffer.readUInt16BE(),
|
telemetry.add({
|
||||||
});
|
'channel': channel,
|
||||||
break;
|
'type': type,
|
||||||
case lppPower:
|
'value': buffer.readUInt16BE(),
|
||||||
telemetry.add({
|
});
|
||||||
'channel': channel,
|
break;
|
||||||
'type': type,
|
case lppGps:
|
||||||
'value': buffer.readUInt16BE(),
|
telemetry.add({
|
||||||
});
|
'channel': channel,
|
||||||
break;
|
'type': type,
|
||||||
case lppGps:
|
'value': {
|
||||||
telemetry.add({
|
'latitude': buffer.readInt24BE() / 10000,
|
||||||
'channel': channel,
|
'longitude': buffer.readInt24BE() / 10000,
|
||||||
'type': type,
|
'altitude': buffer.readInt24BE() / 100,
|
||||||
'value': {
|
},
|
||||||
'latitude': buffer.readInt24BE() / 10000,
|
});
|
||||||
'longitude': buffer.readInt24BE() / 10000,
|
break;
|
||||||
'altitude': buffer.readInt24BE() / 100,
|
default:
|
||||||
},
|
return telemetry;
|
||||||
});
|
}
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return telemetry;
|
|
||||||
}
|
}
|
||||||
|
return telemetry;
|
||||||
|
} catch (e) {
|
||||||
|
// Handle parsing errors, possibly due to malformed data
|
||||||
|
appLogger.error('Error parsing Cayenne LPP data: $e');
|
||||||
|
// Return any telemetry parsed so far to preserve partial data
|
||||||
|
return telemetry;
|
||||||
}
|
}
|
||||||
|
|
||||||
return telemetry;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<Map<String, dynamic>> parseByChannel(Uint8List bytes) {
|
static List<Map<String, dynamic>> parseByChannel(Uint8List bytes) {
|
||||||
final buffer = BufferReader(bytes);
|
final buffer = BufferReader(bytes);
|
||||||
final Map<int, Map<String, dynamic>> channels = {};
|
final Map<int, Map<String, dynamic>> channels = {};
|
||||||
|
try {
|
||||||
|
while (buffer.remaining >= 2) {
|
||||||
|
final channel = buffer.readUInt8();
|
||||||
|
final type = buffer.readUInt8();
|
||||||
|
|
||||||
while (buffer.remaining >= 2) {
|
// Optional: stop on padding (00 00)
|
||||||
final channel = buffer.readUInt8();
|
if (channel == 0 && type == 0) {
|
||||||
final type = buffer.readUInt8();
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Optional: stop on padding (00 00)
|
final channelData = channels.putIfAbsent(
|
||||||
if (channel == 0 && type == 0) {
|
channel,
|
||||||
break;
|
() => {'channel': channel, 'values': <String, dynamic>{}},
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case lppGenericSensor:
|
||||||
|
channelData['values']['generic'] = buffer.readUInt32BE();
|
||||||
|
break;
|
||||||
|
case lppLuminosity:
|
||||||
|
channelData['values']['luminosity'] = buffer.readUInt16BE();
|
||||||
|
break;
|
||||||
|
case lppPresence:
|
||||||
|
channelData['values']['presence'] = buffer.readUInt8() != 0;
|
||||||
|
break;
|
||||||
|
case lppTemperature:
|
||||||
|
channelData['values']['temperature'] = buffer.readInt16BE() / 10.0;
|
||||||
|
break;
|
||||||
|
case lppRelativeHumidity:
|
||||||
|
channelData['values']['humidity'] = buffer.readUInt8() / 2.0;
|
||||||
|
break;
|
||||||
|
case lppBarometricPressure:
|
||||||
|
channelData['values']['pressure'] = buffer.readUInt16BE() / 10.0;
|
||||||
|
break;
|
||||||
|
case lppVoltage:
|
||||||
|
channelData['values']['voltage'] = buffer.readInt16BE() / 100.0;
|
||||||
|
break;
|
||||||
|
case lppCurrent:
|
||||||
|
channelData['values']['current'] = buffer.readInt16BE() / 1000.0;
|
||||||
|
break;
|
||||||
|
case lppPercentage:
|
||||||
|
channelData['values']['percentage'] = buffer.readUInt8();
|
||||||
|
break;
|
||||||
|
case lppConcentration:
|
||||||
|
channelData['values']['concentration'] = buffer.readUInt16BE();
|
||||||
|
break;
|
||||||
|
case lppPower:
|
||||||
|
channelData['values']['power'] = buffer.readUInt16BE();
|
||||||
|
break;
|
||||||
|
case lppGps:
|
||||||
|
channelData['values']['gps'] = {
|
||||||
|
'latitude': buffer.readInt24BE() / 10000.0,
|
||||||
|
'longitude': buffer.readInt24BE() / 10000.0,
|
||||||
|
'altitude': buffer.readInt24BE() / 100.0,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
// Add more types as needed...
|
||||||
|
default:
|
||||||
|
//Stopped parsing to avoid misalignment
|
||||||
|
return channels.values.toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final channelData = channels.putIfAbsent(
|
final List<Map<String, dynamic>> channelsOut = channels.values.toList();
|
||||||
channel,
|
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
|
||||||
() => {'channel': channel, 'values': <String, dynamic>{}},
|
return channelsOut;
|
||||||
);
|
} catch (e) {
|
||||||
|
// Handle parsing errors, possibly due to malformed data
|
||||||
switch (type) {
|
appLogger.error('Error parsing Cayenne LPP data: $e');
|
||||||
case lppGenericSensor:
|
return <
|
||||||
channelData['values']['generic'] = buffer.readUInt32BE();
|
Map<String, dynamic>
|
||||||
break;
|
>[]; // Return an empty list on error to avoid crashing the app
|
||||||
case lppLuminosity:
|
|
||||||
channelData['values']['luminosity'] = buffer.readUInt16BE();
|
|
||||||
break;
|
|
||||||
case lppPresence:
|
|
||||||
channelData['values']['presence'] = buffer.readUInt8() != 0;
|
|
||||||
break;
|
|
||||||
case lppTemperature:
|
|
||||||
channelData['values']['temperature'] = buffer.readInt16BE() / 10.0;
|
|
||||||
break;
|
|
||||||
case lppRelativeHumidity:
|
|
||||||
channelData['values']['humidity'] = buffer.readUInt8() / 2.0;
|
|
||||||
break;
|
|
||||||
case lppBarometricPressure:
|
|
||||||
channelData['values']['pressure'] = buffer.readUInt16BE() / 10.0;
|
|
||||||
break;
|
|
||||||
case lppVoltage:
|
|
||||||
channelData['values']['voltage'] = buffer.readInt16BE() / 100.0;
|
|
||||||
break;
|
|
||||||
case lppCurrent:
|
|
||||||
channelData['values']['current'] = buffer.readInt16BE() / 1000.0;
|
|
||||||
break;
|
|
||||||
case lppPercentage:
|
|
||||||
channelData['values']['percentage'] = buffer.readUInt8();
|
|
||||||
break;
|
|
||||||
case lppConcentration:
|
|
||||||
channelData['values']['concentration'] = buffer.readUInt16BE();
|
|
||||||
break;
|
|
||||||
case lppPower:
|
|
||||||
channelData['values']['power'] = buffer.readUInt16BE();
|
|
||||||
break;
|
|
||||||
case lppGps:
|
|
||||||
channelData['values']['gps'] = {
|
|
||||||
'latitude': buffer.readInt24BE() / 10000.0,
|
|
||||||
'longitude': buffer.readInt24BE() / 10000.0,
|
|
||||||
'altitude': buffer.readInt24BE() / 100.0,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
// Add more types as needed...
|
|
||||||
default:
|
|
||||||
// Unknown type: skip or handle error?
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<Map<String, dynamic>> channelsOut = channels.values.toList();
|
|
||||||
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
|
|
||||||
return channelsOut;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,50 @@
|
|||||||
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 {
|
||||||
|
static TextStyle defaultLinkStyle(BuildContext context, TextStyle base) {
|
||||||
|
final brightness = Theme.of(context).brightness;
|
||||||
|
final orange = brightness == Brightness.dark
|
||||||
|
? const Color(0xFFFFB74D)
|
||||||
|
: const Color(0xFFE65100);
|
||||||
|
return base.copyWith(color: orange, decoration: TextDecoration.underline);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 ?? defaultLinkStyle(context, style);
|
||||||
|
const options = LinkifyOptions(humanize: false, defaultToHttps: false);
|
||||||
|
const linkifiers = [UrlLinkifier(), EmailLinkifier()];
|
||||||
|
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.
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
|
|
||||||
|
class LosIcon extends StatelessWidget {
|
||||||
|
final double size;
|
||||||
|
final Color? color;
|
||||||
|
|
||||||
|
const LosIcon({super.key, this.size = 24, this.color});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final iconTheme = IconTheme.of(context);
|
||||||
|
final iconColor =
|
||||||
|
color ??
|
||||||
|
iconTheme.color ??
|
||||||
|
theme.iconTheme.color ??
|
||||||
|
theme.colorScheme.onSurface;
|
||||||
|
|
||||||
|
return Icon(Symbols.elevation, size: size, color: iconColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
+469
-4
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"channels_channelDeleteFailed": "Неуспешно изтриване на канала \"{name}\"",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@@locale": "bg",
|
"@@locale": "bg",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Контакти",
|
"nav_contacts": "Контакти",
|
||||||
@@ -277,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": {
|
||||||
@@ -334,6 +343,8 @@
|
|||||||
"channels_publicChannel": "Публичен канал",
|
"channels_publicChannel": "Публичен канал",
|
||||||
"channels_privateChannel": "Частен канал",
|
"channels_privateChannel": "Частен канал",
|
||||||
"channels_editChannel": "Редактирай канал",
|
"channels_editChannel": "Редактирай канал",
|
||||||
|
"channels_muteChannel": "Заглуши канала",
|
||||||
|
"channels_unmuteChannel": "Включи известията на канала",
|
||||||
"channels_deleteChannel": "Изтрий канала",
|
"channels_deleteChannel": "Изтрий канала",
|
||||||
"channels_deleteChannelConfirm": "Изтрий \"{name}\"? Това не може да бъде отменено.",
|
"channels_deleteChannelConfirm": "Изтрий \"{name}\"? Това не може да бъде отменено.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
@@ -1351,12 +1362,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighboursSubtitle": "Преглед на съседни възли с нулев скок.",
|
"repeater_neighborsSubtitle": "Преглед на съседни възли с нулев скок.",
|
||||||
"repeater_neighbours": "Съседи",
|
"repeater_neighbors": "Съседи",
|
||||||
"neighbors_receivedData": "Получени данни за съседи",
|
"neighbors_receivedData": "Получени данни за съседи",
|
||||||
"neighbors_requestTimedOut": "Съседите поискат изтичане на време.",
|
"neighbors_requestTimedOut": "Съседите поискат изтичане на време.",
|
||||||
"neighbors_errorLoading": "Грешка при зареждане на съседи: {error}",
|
"neighbors_errorLoading": "Грешка при зареждане на съседи: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Повторители Съседи",
|
"neighbors_repeatersNeighbors": "Повторители Съседи",
|
||||||
"neighbors_noData": "Няма налични данни за съседи.",
|
"neighbors_noData": "Няма налични данни за съседи.",
|
||||||
"channels_createPrivateChannel": "Създай Частен Канал",
|
"channels_createPrivateChannel": "Създай Частен Канал",
|
||||||
"channels_joinPrivateChannel": "Присъедини се към Частен Канал",
|
"channels_joinPrivateChannel": "Присъедини се към Частен Канал",
|
||||||
@@ -1552,6 +1563,8 @@
|
|||||||
"contacts_clipboardEmpty": "Клипборда е празна.",
|
"contacts_clipboardEmpty": "Клипборда е празна.",
|
||||||
"contacts_invalidAdvertFormat": "Невалидни данни за контакт",
|
"contacts_invalidAdvertFormat": "Невалидни данни за контакт",
|
||||||
"appSettings_languageRu": "Руски",
|
"appSettings_languageRu": "Руски",
|
||||||
|
"appSettings_enableMessageTracing": "Разрешаване на проследяване на съобщения",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Показване на подробни метаданни за маршрутизация и синхронизация за съобщения",
|
||||||
"contacts_contactImported": "Контактът е импортиран.",
|
"contacts_contactImported": "Контактът е импортиран.",
|
||||||
"contacts_zeroHopAdvert": "Реклама без скок",
|
"contacts_zeroHopAdvert": "Реклама без скок",
|
||||||
"contacts_contactImportFailed": "Контактът не е успешно импортиран.",
|
"contacts_contactImportFailed": "Контактът не е успешно импортиран.",
|
||||||
@@ -1594,7 +1607,459 @@
|
|||||||
"scanner_bluetoothOff": "Bluetooth е изключен.",
|
"scanner_bluetoothOff": "Bluetooth е изключен.",
|
||||||
"scanner_enableBluetooth": "Активирайте Bluetooth",
|
"scanner_enableBluetooth": "Активирайте Bluetooth",
|
||||||
"scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.",
|
"scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.",
|
||||||
|
"scanner_chromeRequired": "Изисква се браузър Chrome",
|
||||||
|
"scanner_chromeRequiredMessage": "Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.",
|
||||||
|
"snrIndicator_lastSeen": "Последно видян",
|
||||||
|
"snrIndicator_nearByRepeaters": "Близки повтарящи се устройства",
|
||||||
|
"chat_ShowAllPaths": "Покажи всички пътища",
|
||||||
"settings_clientRepeatSubtitle": "Позволете на това устройство да предава пакети към мрежата за други устройства.",
|
"settings_clientRepeatSubtitle": "Позволете на това устройство да предава пакети към мрежата за други устройства.",
|
||||||
"settings_clientRepeatFreqWarning": "За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.",
|
"settings_clientRepeatFreqWarning": "За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.",
|
||||||
"settings_clientRepeat": "Без електричество – повторение"
|
"settings_clientRepeat": "Без електричество – повторение",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)",
|
||||||
|
"appSettings_unitsTitle": "единици",
|
||||||
|
"appSettings_unitsMetric": "Метрика (m / km)",
|
||||||
|
"appSettings_unitsImperial": "Имперска (ft / mi)",
|
||||||
|
"map_lineOfSight": "Линия на видимост",
|
||||||
|
"map_losScreenTitle": "Линия на видимост",
|
||||||
|
"losSelectStartEnd": "Изберете начални и крайни възли за LOS.",
|
||||||
|
"losRunFailed": "Проверката на пряката видимост е неуспешна: {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Изчистете всички точки",
|
||||||
|
"losRunToViewElevationProfile": "Стартирайте LOS, за да видите профила на надморската височина",
|
||||||
|
"losMenuTitle": "LOS меню",
|
||||||
|
"losMenuSubtitle": "Докоснете възли или натиснете продължително карта за персонализирани точки",
|
||||||
|
"losShowDisplayNodes": "Показване на възли на дисплея",
|
||||||
|
"losCustomPoints": "Персонализирани точки",
|
||||||
|
"losCustomPointLabel": "Персонализирано {index}",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Точка А",
|
||||||
|
"losPointB": "Точка Б",
|
||||||
|
"losAntennaA": "Антена A: {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Антена B: {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Стартирайте LOS",
|
||||||
|
"losNoElevationData": "Няма данни за надморска височина",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, чист LOS, минимално разстояние {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, блокиран от {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "LOS: проверка...",
|
||||||
|
"losStatusNoData": "LOS: няма данни",
|
||||||
|
"losStatusSummary": "LOS: {clear}/{total} ясно, {blocked} блокирано, {unknown} неизвестно",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Няма налични данни за надморска височина за една или повече проби.",
|
||||||
|
"losErrorInvalidInput": "Невалидни данни за точки/надморска височина за изчисляване на LOS.",
|
||||||
|
"losRenameCustomPoint": "Преименувайте персонализирана точка",
|
||||||
|
"losPointName": "Име на точката",
|
||||||
|
"losShowPanelTooltip": "Показване на LOS панел",
|
||||||
|
"losHidePanelTooltip": "Скриване на LOS панела",
|
||||||
|
"losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Радиохоризонт",
|
||||||
|
"losLegendLosBeam": "Линия на видимост",
|
||||||
|
"losLegendTerrain": "Терен",
|
||||||
|
"losFrequencyLabel": "Честота",
|
||||||
|
"losFrequencyInfoTooltip": "Преглед на детайли за изчислението",
|
||||||
|
"losFrequencyDialogTitle": "Изчисляване на радиохоризонта",
|
||||||
|
"losFrequencyDialogDescription": "Започвайки от k={baselineK} при {baselineFreq} MHz, изчислението коригира k-фактора за текущата {frequencyMHz} MHz лента, която определя границата на извития радиохоризонт.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listFilter_removeFromFavorites": "Премахване от списъка с любими",
|
||||||
|
"listFilter_addToFavorites": "Добави към любими",
|
||||||
|
"listFilter_favorites": "Любими",
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_searchFavorites": "Търсене на {number}{str} любими...",
|
||||||
|
"contacts_searchRoomServers": "Търсене на {number}{str} сървъри в стаята...",
|
||||||
|
"contacts_unread": "Непрочетено",
|
||||||
|
"contacts_searchRepeaters": "Търсене на {number}{str} повтарящи се...",
|
||||||
|
"contacts_searchContactsNoNumber": "Търси контакти...",
|
||||||
|
"contacts_searchUsers": "Търсене на {number}{str} потребители...",
|
||||||
|
"contactsSettings_title": "Настройки на контактите",
|
||||||
|
"contactsSettings_autoAddTitle": "Автоматично откриване",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Автоматично добавяне на потребители",
|
||||||
|
"contactsSettings_otherTitle": "Други настройки свързани с контакти",
|
||||||
|
"settings_contactSettingsSubtitle": "Настройки за добавяне на контакти.",
|
||||||
|
"settings_contactSettings": "Настройки за контакти",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Автоматично добавяне на датчици",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Автоматично добавяне на сървъри на стаите",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Позволи на спътника да добавя автоматично откритите сървъри на стаите.",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Автоматично добавяне на повтарящи се елементи",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Позволи на спътника да добавя автоматично откритите потребители.",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Позволи на спътника да добавя автоматично откритите повтарящи се устройства.",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Позволи на спътника да добавя автоматично откритите датчици.",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Премахни най-старото",
|
||||||
|
"discoveredContacts_Title": "Открити контакти",
|
||||||
|
"discoveredContacts_searchHint": "Търсене на открити контакти",
|
||||||
|
"discoveredContacts_noMatching": "Няма съвпадащи контакти",
|
||||||
|
"discoveredContacts_contactAdded": "Контакт добавен",
|
||||||
|
"discoveredContacts_copyContact": "Копирай контакт в клипборда",
|
||||||
|
"discoveredContacts_deleteContact": "Изтрий контакт",
|
||||||
|
"discoveredContacts_addContact": "Добави контакт",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Когато списъкът с контакти е пълен, най-старият неключов контакт ще бъде заменен.",
|
||||||
|
"discoveredContacts_deleteContactAll": "Изтриване на Всички Открити Контакти",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Сигурни ли сте, че искате да изтриете всички открити контакти?",
|
||||||
|
"common_deleteAll": "Изтрий всичко",
|
||||||
|
"map_guessedLocation": "Предполагано местоположение",
|
||||||
|
"map_showGuessedLocations": "Покажете местоположенията на предположените възли.",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenTitle": "Свържете се чрез USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenSubtitle": "Изберете открития сериен уред и свържете директно към вашия MeshCore възел.",
|
||||||
|
"usbScreenStatus": "Изберете USB устройство",
|
||||||
|
"usbScreenNote": "USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.",
|
||||||
|
"usbScreenEmptyState": "Няма открити USB устройства. Включете едно и опитайте отново.",
|
||||||
|
"usbErrorPermissionDenied": "Не беше разрешено достъпът през USB.",
|
||||||
|
"usbErrorDeviceMissing": "Избраното USB устройство вече не е налично.",
|
||||||
|
"usbErrorInvalidPort": "Изберете валитно USB устройство.",
|
||||||
|
"usbErrorBusy": "Друг мол за свързване през USB вече е в процес на изпълнение.",
|
||||||
|
"usbErrorNotConnected": "Няма свързано USB устройство.",
|
||||||
|
"usbErrorOpenFailed": "Не успях да отворя избраното USB устройство.",
|
||||||
|
"usbErrorConnectFailed": "Не успях да се свържа с избраното USB устройство.",
|
||||||
|
"usbErrorUnsupported": "USB серийната комуникация не се поддържа на тази платформа.",
|
||||||
|
"usbErrorAlreadyActive": "USB връзката вече е активирана.",
|
||||||
|
"usbErrorNoDeviceSelected": "Няма избран USB устройство.",
|
||||||
|
"usbErrorPortClosed": "USB връзката не е активна.",
|
||||||
|
"usbFallbackDeviceName": "Устройство за четене на уеб серийни данни",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_connecting": "Свързване към USB устройство...",
|
||||||
|
"usbConnectionFailed": "Неуспешно свързване през USB: {error}",
|
||||||
|
"usbStatus_notConnected": "Изберете USB устройство",
|
||||||
|
"usbStatus_searching": "Търсене на USB устройства...",
|
||||||
|
"usbErrorConnectTimedOut": "Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка.",
|
||||||
|
"@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": "Върни се по същия път.",
|
||||||
|
"@radioStats_noiseFloor": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_lastRssi": {
|
||||||
|
"placeholders": {
|
||||||
|
"rssiDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_lastSnr": {
|
||||||
|
"placeholders": {
|
||||||
|
"snr": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_txAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_rxAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_stripNoise": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chat_sendCooldown": "Моля, изчакайте малко, преди да изпратите отново.",
|
||||||
|
"appSettings_languageHu": "Унгарски",
|
||||||
|
"appSettings_jumpToOldestUnread": "Преминете към най-старата непочетена статия",
|
||||||
|
"appSettings_jumpToOldestUnreadSubtitle": "Когато отворите чат с непрочетени съобщения, плъзнете надолу, за да видите първото непрочетено съобщение, вместо най-новото.",
|
||||||
|
"appSettings_languageJa": "Японски",
|
||||||
|
"appSettings_languageKo": "Корейски",
|
||||||
|
"radioStats_tooltip": "Статистика за радио и мрежа",
|
||||||
|
"radioStats_screenTitle": "Статистически данни за радиопредаванията",
|
||||||
|
"radioStats_notConnected": "Свържете се с устройство, за да видите статистически данни за радиопредаване.",
|
||||||
|
"radioStats_firmwareTooOld": "Статистиката на радиостанцията изисква съвместимо софтуерно решение версия 8 или по-нова.",
|
||||||
|
"radioStats_waiting": "Изчакване на данни…",
|
||||||
|
"radioStats_noiseFloor": "Ниво на шума: {noiseDbm} dBm",
|
||||||
|
"radioStats_lastRssi": "Последен RSSI: {rssiDbm} dBm",
|
||||||
|
"radioStats_lastSnr": "Последна стойност на SNR: {snr} dB",
|
||||||
|
"radioStats_txAir": "Време на въздух (общо): {seconds} секунди",
|
||||||
|
"radioStats_rxAir": "Общо време на използване на RX (в секунди): {seconds} с",
|
||||||
|
"radioStats_chartCaption": "Ниво на шума (dBm) за последните измервания.",
|
||||||
|
"radioStats_stripNoise": "Ниво на шума: {noiseDbm} dBm",
|
||||||
|
"radioStats_stripWaiting": "Извличане на данни за радиото…",
|
||||||
|
"radioStats_settingsTile": "Статистически данни за радиостанции",
|
||||||
|
"radioStats_settingsSubtitle": "Ниво на шума, RSSI, SNR и време на пренос",
|
||||||
|
"@translation_downloadFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_enableTitle": "Активирайте превода",
|
||||||
|
"translation_title": "Превод",
|
||||||
|
"translation_composerTitle": "Преведете преди да изпратите",
|
||||||
|
"translation_enableSubtitle": "Превеждайте входящите съобщения и позволявайте предварително превеждане преди изпращане.",
|
||||||
|
"translation_composerSubtitle": "Контролира началния статус на иконата за превод, създадена от композитора.",
|
||||||
|
"translation_targetLanguage": "Целеви език",
|
||||||
|
"translation_useAppLanguage": "Използвайте езика на приложението",
|
||||||
|
"translation_downloadedModelLabel": "Изтегнат модел",
|
||||||
|
"translation_presetModelLabel": "Предварително конфигуриран модел от Hugging Face",
|
||||||
|
"translation_manualUrlLabel": "URL на ръководството",
|
||||||
|
"translation_downloadModel": "Изтеглете модела",
|
||||||
|
"translation_downloading": "Изтегляне...",
|
||||||
|
"translation_working": "Работа...",
|
||||||
|
"translation_stop": "Спрете",
|
||||||
|
"translation_mergingChunks": "Съединяване на изтеглените части в един файл...",
|
||||||
|
"translation_downloadedModels": "Изтеглени модели",
|
||||||
|
"translation_deleteModel": "Изтриване на модела",
|
||||||
|
"translation_modelDownloaded": "Моделът за превод е изтеглен.",
|
||||||
|
"translation_downloadStopped": "Изтеглянето беше прекъснато.",
|
||||||
|
"translation_downloadFailed": "Не успях да изтегля: {error}",
|
||||||
|
"translation_enterUrlFirst": "Въведете първо URL адрес на модела.",
|
||||||
|
"@scanner_linuxPairingPinPrompt": {
|
||||||
|
"placeholders": {
|
||||||
|
"deviceName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@translation_translateTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"language": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_composerDisabledHint": "Изпращайте съобщения на оригиналния въведен език.",
|
||||||
|
"translation_translateBeforeSending": "Преведете преди да изпратите",
|
||||||
|
"translation_messageTranslation": "Превод на съобщението",
|
||||||
|
"translation_composerEnabledHint": "Съобщенията ще бъдат преведени, преди да бъдат изпратени.",
|
||||||
|
"translation_translateTo": "Превеждане на {language}",
|
||||||
|
"translation_translationOptions": "Опции за превод",
|
||||||
|
"translation_systemLanguage": "Език на системата",
|
||||||
|
"scanner_linuxPairingPinTitle": "PIN за съвпадение чрез Bluetooth",
|
||||||
|
"scanner_linuxPairingPinPrompt": "Въведете PIN кода за {deviceName} (оставете празно, ако няма такъв).",
|
||||||
|
"scanner_linuxPairingHidePin": "Скриване на PIN кода",
|
||||||
|
"scanner_linuxPairingShowPin": "Покажи PIN",
|
||||||
|
"repeater_cliQuickClockSync": "Синхронизация на часовника",
|
||||||
|
"repeater_cliQuickDiscovery": "Открий Съседи"
|
||||||
}
|
}
|
||||||
+475
-10
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"channels_channelDeleteFailed": "Kanal {name} konnte nicht gelöscht werden",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@@locale": "de",
|
"@@locale": "de",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Kontakte",
|
"nav_contacts": "Kontakte",
|
||||||
@@ -277,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": {
|
||||||
@@ -288,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": {
|
||||||
@@ -297,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": {
|
||||||
@@ -306,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": {
|
||||||
@@ -334,6 +343,8 @@
|
|||||||
"channels_publicChannel": "Öffentlicher Kanal",
|
"channels_publicChannel": "Öffentlicher Kanal",
|
||||||
"channels_privateChannel": "Privater Kanal",
|
"channels_privateChannel": "Privater Kanal",
|
||||||
"channels_editChannel": "Kanal bearbeiten",
|
"channels_editChannel": "Kanal bearbeiten",
|
||||||
|
"channels_muteChannel": "Kanal stummschalten",
|
||||||
|
"channels_unmuteChannel": "Kanal Stummschaltung aufheben",
|
||||||
"channels_deleteChannel": "Lösche den Kanal",
|
"channels_deleteChannel": "Lösche den Kanal",
|
||||||
"channels_deleteChannelConfirm": "Löschen von \"{name}\"? Dies kann nicht rückgängig gemacht werden.",
|
"channels_deleteChannelConfirm": "Löschen von \"{name}\"? Dies kann nicht rückgängig gemacht werden.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
@@ -1339,6 +1350,9 @@
|
|||||||
"listFilter_az": "A-Z",
|
"listFilter_az": "A-Z",
|
||||||
"listFilter_filters": "Filtere",
|
"listFilter_filters": "Filtere",
|
||||||
"listFilter_all": "Alle",
|
"listFilter_all": "Alle",
|
||||||
|
"listFilter_favorites": "Favoriten",
|
||||||
|
"listFilter_addToFavorites": "Zu Favoriten hinzufügen",
|
||||||
|
"listFilter_removeFromFavorites": "Aus Favoriten entfernen",
|
||||||
"listFilter_users": "Benutzer",
|
"listFilter_users": "Benutzer",
|
||||||
"listFilter_repeaters": "Repeater",
|
"listFilter_repeaters": "Repeater",
|
||||||
"listFilter_roomServers": "Raumserver",
|
"listFilter_roomServers": "Raumserver",
|
||||||
@@ -1351,12 +1365,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Nachbarn",
|
"repeater_neighbors": "Nachbarn",
|
||||||
"repeater_neighboursSubtitle": "Anzahl der Hop-Nachbarn anzeigen.",
|
"repeater_neighborsSubtitle": "Anzahl der Hop-Nachbarn anzeigen.",
|
||||||
"neighbors_receivedData": "Empfangene Nachbarsdaten",
|
"neighbors_receivedData": "Empfangene Nachbarsdaten",
|
||||||
"neighbors_requestTimedOut": "Anfrage durch Timeout fehlgeschlagen.",
|
"neighbors_requestTimedOut": "Anfrage durch Timeout fehlgeschlagen.",
|
||||||
"neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}",
|
"neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Nachbarn",
|
"neighbors_repeatersNeighbors": "Nachbarn",
|
||||||
"neighbors_noData": "Keine Nachbarsdaten verfügbar.",
|
"neighbors_noData": "Keine Nachbarsdaten verfügbar.",
|
||||||
"channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei",
|
"channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei",
|
||||||
"channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.",
|
"channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.",
|
||||||
@@ -1552,6 +1566,8 @@
|
|||||||
"contacts_invalidAdvertFormat": "Ungültige Kontaktdaten",
|
"contacts_invalidAdvertFormat": "Ungültige Kontaktdaten",
|
||||||
"contacts_clipboardEmpty": "Die Zwischenablage ist leer.",
|
"contacts_clipboardEmpty": "Die Zwischenablage ist leer.",
|
||||||
"appSettings_languageUk": "Ukrainisch",
|
"appSettings_languageUk": "Ukrainisch",
|
||||||
|
"appSettings_enableMessageTracing": "Nachrichtenverfolgung aktivieren",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen",
|
||||||
"contacts_contactImported": "Kontakt wurde importiert.",
|
"contacts_contactImported": "Kontakt wurde importiert.",
|
||||||
"contacts_contactImportFailed": "Kontakt konnte nicht importiert werden",
|
"contacts_contactImportFailed": "Kontakt konnte nicht importiert werden",
|
||||||
"contacts_zeroHopAdvert": "Zero-Hop-Ankündigung",
|
"contacts_zeroHopAdvert": "Zero-Hop-Ankündigung",
|
||||||
@@ -1620,9 +1636,458 @@
|
|||||||
"pathTrace_clearTooltip": "Pfad löschen",
|
"pathTrace_clearTooltip": "Pfad löschen",
|
||||||
"map_pathTraceCancelled": "Pfadverfolgung abgebrochen.",
|
"map_pathTraceCancelled": "Pfadverfolgung abgebrochen.",
|
||||||
"scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.",
|
"scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.",
|
||||||
|
"scanner_chromeRequired": "Chrome Browser erforderlich",
|
||||||
|
"scanner_chromeRequiredMessage": "Diese Webanwendung erfordert Google Chrome oder einen Chromium-basierten Browser für die Bluetooth-Unterstützung.",
|
||||||
"scanner_bluetoothOff": "Bluetooth ist deaktiviert.",
|
"scanner_bluetoothOff": "Bluetooth ist deaktiviert.",
|
||||||
"scanner_enableBluetooth": "Bluetooth aktivieren",
|
"scanner_enableBluetooth": "Bluetooth aktivieren",
|
||||||
|
"snrIndicator_lastSeen": "Zuletzt gesehen",
|
||||||
|
"snrIndicator_nearByRepeaters": "In der Nähe befindliche Repeater",
|
||||||
|
"chat_ShowAllPaths": "Alle Pfade anzeigen",
|
||||||
"settings_clientRepeat": "Wiederholung, ohne Stromanschluss",
|
"settings_clientRepeat": "Wiederholung, ohne Stromanschluss",
|
||||||
"settings_clientRepeatFreqWarning": "Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.",
|
"settings_clientRepeatFreqWarning": "Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.",
|
||||||
"settings_clientRepeatSubtitle": "Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen."
|
"settings_clientRepeatSubtitle": "Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen.",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "LOS-Höhendaten: Open-Meteo (CC BY 4.0)",
|
||||||
|
"appSettings_unitsTitle": "Einheiten",
|
||||||
|
"appSettings_unitsMetric": "Metrisch (m/km)",
|
||||||
|
"appSettings_unitsImperial": "Imperial (ft/mi)",
|
||||||
|
"map_lineOfSight": "Sichtlinie",
|
||||||
|
"map_losScreenTitle": "Sichtlinie",
|
||||||
|
"losSelectStartEnd": "Wählen Sie Start- und Endknoten für LOS aus.",
|
||||||
|
"losRunFailed": "Sichtlinienprüfung fehlgeschlagen: {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Löschen Sie alle Punkte",
|
||||||
|
"losRunToViewElevationProfile": "Führen Sie LOS aus, um das Höhenprofil anzuzeigen",
|
||||||
|
"losMenuTitle": "LOS-Menü",
|
||||||
|
"losMenuSubtitle": "Tippen Sie auf Knoten oder drücken Sie lange auf die Karte, um benutzerdefinierte Punkte anzuzeigen",
|
||||||
|
"losShowDisplayNodes": "Anzeigeknoten anzeigen",
|
||||||
|
"losCustomPoints": "Benutzerdefinierte Punkte",
|
||||||
|
"losCustomPointLabel": "Benutzerdefiniert {index}",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Punkt A",
|
||||||
|
"losPointB": "Punkt B",
|
||||||
|
"losAntennaA": "Antenne A: {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Antenne B: {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Führen Sie LOS aus",
|
||||||
|
"losNoElevationData": "Keine Höhendaten",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, freie Sichtlinie, Mindestabstand {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, blockiert durch {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "LOS: Überprüfen...",
|
||||||
|
"losStatusNoData": "LOS: keine Daten",
|
||||||
|
"losStatusSummary": "Sichtlinie: {clear}/{total} frei, {blocked} blockiert, {unknown} unbekannt",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Für eine oder mehrere Proben sind keine Höhendaten verfügbar.",
|
||||||
|
"losErrorInvalidInput": "Ungültige Punkte/Höhendaten für die LOS-Berechnung.",
|
||||||
|
"losRenameCustomPoint": "Benennen Sie den benutzerdefinierten Punkt um",
|
||||||
|
"losPointName": "Punktname",
|
||||||
|
"losShowPanelTooltip": "LOS-Panel anzeigen",
|
||||||
|
"losHidePanelTooltip": "LOS-Panel ausblenden",
|
||||||
|
"losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Funkhorizont",
|
||||||
|
"losLegendLosBeam": "Sichtlinie",
|
||||||
|
"losLegendTerrain": "Gelände",
|
||||||
|
"losFrequencyLabel": "Frequenz",
|
||||||
|
"losFrequencyInfoTooltip": "Details zur Berechnung anzeigen",
|
||||||
|
"losFrequencyDialogTitle": "Berechnung des Funkhorizonts",
|
||||||
|
"losFrequencyDialogDescription": "Ausgehend von k={baselineK} bei {baselineFreq} MHz passt die Berechnung den k-Faktor für das aktuelle {frequencyMHz} MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_unread": "Ungelesen",
|
||||||
|
"contacts_searchContactsNoNumber": "Kontakte suchen...",
|
||||||
|
"contacts_searchRepeaters": "Suche {number}{str} Repeater...",
|
||||||
|
"contacts_searchFavorites": "Suche {number}{str} Favoriten...",
|
||||||
|
"contacts_searchUsers": "Suche {number}{str} Benutzer...",
|
||||||
|
"contacts_searchRoomServers": "Suche {number}{str} Raumserver...",
|
||||||
|
"settings_contactSettings": "Kontakteinstellungen",
|
||||||
|
"contactsSettings_otherTitle": "Weitere Einstellungen zu Kontakten",
|
||||||
|
"contactsSettings_title": "Kontakteinstellungen",
|
||||||
|
"contactsSettings_autoAddTitle": "Automatische Erkennung",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Automatische Hinzufügung von Benutzern",
|
||||||
|
"settings_contactSettingsSubtitle": "Einstellungen für das Hinzufügen von Kontakten",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Automatisch Sensoren hinzufügen",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Benutzer hinzuzufügen",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Automatisch Raumservers hinzufügen",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Ermöglichen Sie dem Begleiter, entdeckte Raumserver automatisch hinzuzufügen",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Automatisch Repeater hinzufügen",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Repeater hinzuzufügen.",
|
||||||
|
"discoveredContacts_noMatching": "Keine passenden Kontakte",
|
||||||
|
"discoveredContacts_searchHint": "Entdeckte Kontakte suchen",
|
||||||
|
"discoveredContacts_addContact": "Kontakt hinzufügen",
|
||||||
|
"discoveredContacts_contactAdded": "Kontakt hinzugefügt",
|
||||||
|
"discoveredContacts_deleteContact": "Kontakt löschen",
|
||||||
|
"discoveredContacts_Title": "Entdeckte Kontakte",
|
||||||
|
"discoveredContacts_copyContact": "Kontakt in die Zwischenablage kopieren",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Überschreiben des Ältesten",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Sensoren hinzuzufügen",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Wenn die Kontaktliste voll ist, wird der älteste nicht favorisierte Kontakt ersetzt.",
|
||||||
|
"common_deleteAll": "Alles löschen",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Sind Sie sicher, dass Sie alle gefundenen Kontakte löschen möchten?",
|
||||||
|
"discoveredContacts_deleteContactAll": "Alle entdeckten Kontakte löschen",
|
||||||
|
"map_showGuessedLocations": "Zeige die vermuteten Knotenpositionen",
|
||||||
|
"map_guessedLocation": "Geschätzter Ort",
|
||||||
|
"usbScreenSubtitle": "Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenTitle": "Verbinden über USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenStatus": "Wählen Sie ein USB-Gerät aus",
|
||||||
|
"usbScreenNote": "Die USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.",
|
||||||
|
"usbScreenEmptyState": "Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie.",
|
||||||
|
"usbErrorPermissionDenied": "Die USB-Berechtigung wurde abgelehnt.",
|
||||||
|
"usbErrorDeviceMissing": "Das ausgewählte USB-Gerät ist nicht mehr verfügbar.",
|
||||||
|
"usbErrorInvalidPort": "Wählen Sie ein gültiges USB-Gerät aus.",
|
||||||
|
"usbErrorBusy": "Eine weitere Anfrage für eine USB-Verbindung ist bereits in Bearbeitung.",
|
||||||
|
"usbErrorNotConnected": "Es ist kein USB-Gerät angeschlossen.",
|
||||||
|
"usbErrorOpenFailed": "Fehlgeschlagen beim Öffnen des ausgewählten USB-Geräts.",
|
||||||
|
"usbErrorConnectFailed": "Keine Verbindung zum ausgewählten USB-Gerät hergestellt.",
|
||||||
|
"usbErrorUnsupported": "Die USB-Serielle Schnittstelle wird auf dieser Plattform nicht unterstützt.",
|
||||||
|
"usbErrorAlreadyActive": "Eine USB-Verbindung ist bereits hergestellt.",
|
||||||
|
"usbErrorNoDeviceSelected": "Kein USB-Gerät wurde ausgewählt.",
|
||||||
|
"usbErrorPortClosed": "Die USB-Verbindung ist nicht aktiv.",
|
||||||
|
"usbFallbackDeviceName": "Web-Serielle Geräte",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_searching": "Suche nach USB-Geräten...",
|
||||||
|
"usbStatus_notConnected": "Wählen Sie ein USB-Gerät aus",
|
||||||
|
"usbStatus_connecting": "Verbindung zum USB-Gerät...",
|
||||||
|
"usbConnectionFailed": "Fehler beim USB-Verbindungsaufbau: {error}",
|
||||||
|
"usbErrorConnectTimedOut": "Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält.",
|
||||||
|
"@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.",
|
||||||
|
"@radioStats_noiseFloor": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_lastRssi": {
|
||||||
|
"placeholders": {
|
||||||
|
"rssiDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_lastSnr": {
|
||||||
|
"placeholders": {
|
||||||
|
"snr": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_txAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_rxAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_stripNoise": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chat_sendCooldown": "Bitte warten Sie einen Moment, bevor Sie erneut senden.",
|
||||||
|
"appSettings_jumpToOldestUnread": "Zum ältesten, nicht gelesenen Eintrag springen",
|
||||||
|
"appSettings_languageHu": "Ungarisch",
|
||||||
|
"appSettings_jumpToOldestUnreadSubtitle": "Wenn Sie ein Chatfenster öffnen, in dem Nachrichten vorhanden sind, die noch nicht gelesen wurden, scrollen Sie zu der ersten unlesenen Nachricht, anstatt zur neuesten.",
|
||||||
|
"appSettings_languageJa": "Japanisch",
|
||||||
|
"appSettings_languageKo": "Koreanisch",
|
||||||
|
"radioStats_tooltip": "Daten zu Radio- und Mesh-Netzwerken",
|
||||||
|
"radioStats_screenTitle": "Senderinformationen",
|
||||||
|
"radioStats_notConnected": "Verbinden Sie ein Gerät, um Radiostatisiken anzuzeigen.",
|
||||||
|
"radioStats_firmwareTooOld": "Für die Verwendung der Funkstatistiken ist die Firmware-Version 8 oder höher erforderlich.",
|
||||||
|
"radioStats_waiting": "Warte auf Daten…",
|
||||||
|
"radioStats_noiseFloor": "Rauschpegel: {noiseDbm} dBm",
|
||||||
|
"radioStats_lastRssi": "Letzter RSSI-Wert: {rssiDbm} dBm",
|
||||||
|
"radioStats_lastSnr": "Letzter SNR: {snr} dB",
|
||||||
|
"radioStats_txAir": "Gesamt-TX-Zeit: {seconds} s",
|
||||||
|
"radioStats_rxAir": "Gesamt-RX-Zeit: {seconds} s",
|
||||||
|
"radioStats_chartCaption": "Rauschpegel (dBm) basierend auf den letzten Messwerten.",
|
||||||
|
"radioStats_stripNoise": "Rauschpegel: {noiseDbm} dBm",
|
||||||
|
"radioStats_stripWaiting": "Abrufen von Radiostatus…",
|
||||||
|
"radioStats_settingsTile": "Senderinformationen",
|
||||||
|
"radioStats_settingsSubtitle": "Rauschpegel, RSSI, Signal-Rausch-Verhältnis (SNR) und Nutzzeit",
|
||||||
|
"@translation_downloadFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_title": "Übersetzung",
|
||||||
|
"translation_composerTitle": "Übersetzen Sie vor dem Versenden",
|
||||||
|
"translation_enableSubtitle": "Nachrichten empfangen und übersetzen sowie die Möglichkeit bieten, Nachrichten vor dem Versenden zu übersetzen.",
|
||||||
|
"translation_enableTitle": "Aktivieren Sie die Übersetzung",
|
||||||
|
"translation_composerSubtitle": "Steuert den Standardzustand des Icons für die Übersetzung des Komponisten.",
|
||||||
|
"translation_targetLanguage": "Zielsprache",
|
||||||
|
"translation_useAppLanguage": "Verwenden Sie die App-Sprache",
|
||||||
|
"translation_downloadedModelLabel": "Heruntergeladenes Modell",
|
||||||
|
"translation_presetModelLabel": "Vordefinierter Hugging Face-Modell",
|
||||||
|
"translation_manualUrlLabel": "URL für das manuelle Modell",
|
||||||
|
"translation_downloadModel": "Modell herunterladen",
|
||||||
|
"translation_downloading": "Herunterladen...",
|
||||||
|
"translation_working": "Arbeiten...",
|
||||||
|
"translation_stop": "Stopp",
|
||||||
|
"translation_mergingChunks": "Zusammenführen der heruntergeladenen Teile in die finale Datei...",
|
||||||
|
"translation_downloadedModels": "Heruntergeladene Modelle",
|
||||||
|
"translation_deleteModel": "Modell löschen",
|
||||||
|
"translation_modelDownloaded": "Übersetzungsmotor heruntergeladen.",
|
||||||
|
"translation_downloadStopped": "Herunterladen abgebrochen.",
|
||||||
|
"translation_downloadFailed": "Download fehlgeschlagen: {error}",
|
||||||
|
"translation_enterUrlFirst": "Geben Sie zunächst die URL eines Modells ein.",
|
||||||
|
"@scanner_linuxPairingPinPrompt": {
|
||||||
|
"placeholders": {
|
||||||
|
"deviceName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@translation_translateTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"language": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_messageTranslation": "Nachricht übersetzen",
|
||||||
|
"translation_composerEnabledHint": "Die Nachrichten werden vor dem Versenden übersetzt.",
|
||||||
|
"translation_translateBeforeSending": "Übersetzen Sie vor dem Versenden",
|
||||||
|
"translation_composerDisabledHint": "Nachrichten in der ursprünglichen, getippten Sprache senden.",
|
||||||
|
"translation_translateTo": "Übersetzen Sie auf {language}",
|
||||||
|
"translation_translationOptions": "Übersetzungsmöglichkeiten",
|
||||||
|
"translation_systemLanguage": "Sprache des Systems",
|
||||||
|
"scanner_linuxPairingShowPin": "PIN anzeigen",
|
||||||
|
"scanner_linuxPairingHidePin": "PIN ausblenden",
|
||||||
|
"scanner_linuxPairingPinTitle": "Bluetooth-Paarungs-PIN",
|
||||||
|
"scanner_linuxPairingPinPrompt": "Geben Sie die PIN für {deviceName} ein (leer lassen, falls keine).",
|
||||||
|
"repeater_cliQuickClockSync": "Uhr Synchronisieren",
|
||||||
|
"repeater_cliQuickDiscovery": "Entdecke Nachbarn"
|
||||||
}
|
}
|
||||||
+501
-15
@@ -10,6 +10,7 @@
|
|||||||
"common_unknownDevice": "Unknown Device",
|
"common_unknownDevice": "Unknown Device",
|
||||||
"common_save": "Save",
|
"common_save": "Save",
|
||||||
"common_delete": "Delete",
|
"common_delete": "Delete",
|
||||||
|
"common_deleteAll": "Delete All",
|
||||||
"common_close": "Close",
|
"common_close": "Close",
|
||||||
"common_edit": "Edit",
|
"common_edit": "Edit",
|
||||||
"common_add": "Add",
|
"common_add": "Add",
|
||||||
@@ -46,6 +47,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...",
|
||||||
@@ -68,10 +127,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"scanner_stop": "Stop",
|
"scanner_stop": "Stop",
|
||||||
"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 +160,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",
|
||||||
@@ -131,6 +215,7 @@
|
|||||||
},
|
},
|
||||||
"settings_aboutLegalese": "2026 MeshCore Open Source Project",
|
"settings_aboutLegalese": "2026 MeshCore Open Source Project",
|
||||||
"settings_aboutDescription": "An open-source Flutter client for MeshCore LoRa mesh networking devices.",
|
"settings_aboutDescription": "An open-source Flutter client for MeshCore LoRa mesh networking devices.",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "LOS elevation data: Open-Meteo (CC BY 4.0)",
|
||||||
"settings_infoName": "Name",
|
"settings_infoName": "Name",
|
||||||
"settings_infoId": "ID",
|
"settings_infoId": "ID",
|
||||||
"settings_infoStatus": "Status",
|
"settings_infoStatus": "Status",
|
||||||
@@ -182,6 +267,8 @@
|
|||||||
"appSettings_languageBg": "Български",
|
"appSettings_languageBg": "Български",
|
||||||
"appSettings_languageRu": "Русский",
|
"appSettings_languageRu": "Русский",
|
||||||
"appSettings_languageUk": "Українська",
|
"appSettings_languageUk": "Українська",
|
||||||
|
"appSettings_enableMessageTracing": "Enable Message Tracing",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Show detailed routing and timing metadata for messages",
|
||||||
"appSettings_notifications": "Notifications",
|
"appSettings_notifications": "Notifications",
|
||||||
"appSettings_enableNotifications": "Enable Notifications",
|
"appSettings_enableNotifications": "Enable Notifications",
|
||||||
"appSettings_enableNotificationsSubtitle": "Receive notifications for messages and adverts",
|
"appSettings_enableNotificationsSubtitle": "Receive notifications for messages and adverts",
|
||||||
@@ -203,6 +290,27 @@
|
|||||||
"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})",
|
||||||
@@ -242,6 +350,9 @@
|
|||||||
"appSettings_last24Hours": "Last 24 hours",
|
"appSettings_last24Hours": "Last 24 hours",
|
||||||
"appSettings_lastWeek": "Last week",
|
"appSettings_lastWeek": "Last week",
|
||||||
"appSettings_offlineMapCache": "Offline Map Cache",
|
"appSettings_offlineMapCache": "Offline Map Cache",
|
||||||
|
"appSettings_unitsTitle": "Units",
|
||||||
|
"appSettings_unitsMetric": "Metric (m / km)",
|
||||||
|
"appSettings_unitsImperial": "Imperial (ft / mi)",
|
||||||
"appSettings_noAreaSelected": "No area selected",
|
"appSettings_noAreaSelected": "No area selected",
|
||||||
"appSettings_areaSelectedZoom": "Area selected (zoom {minZoom}-{maxZoom})",
|
"appSettings_areaSelectedZoom": "Area selected (zoom {minZoom}-{maxZoom})",
|
||||||
"@appSettings_areaSelectedZoom": {
|
"@appSettings_areaSelectedZoom": {
|
||||||
@@ -262,7 +373,63 @@
|
|||||||
"contacts_title": "Contacts",
|
"contacts_title": "Contacts",
|
||||||
"contacts_noContacts": "No contacts yet",
|
"contacts_noContacts": "No contacts yet",
|
||||||
"contacts_contactsWillAppear": "Contacts will appear when devices advertise",
|
"contacts_contactsWillAppear": "Contacts will appear when devices advertise",
|
||||||
"contacts_searchContacts": "Search contacts...",
|
"contacts_unread": "Unread",
|
||||||
|
"contacts_searchContactsNoNumber": "Search Contacts...",
|
||||||
|
"contacts_searchContacts": "Search {number}{str} Contacts...",
|
||||||
|
"@contacts_searchContacts": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_searchFavorites": "Search {number}{str} Favorites...",
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_searchUsers": "Search {number}{str} Users...",
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_searchRepeaters": "Search {number}{str} Repeaters...",
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_searchRoomServers": "Search {number}{str} Room servers...",
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"contacts_noUnreadContacts": "No unread contacts",
|
"contacts_noUnreadContacts": "No unread contacts",
|
||||||
"contacts_noContactsFound": "No contacts or groups found",
|
"contacts_noContactsFound": "No contacts or groups found",
|
||||||
"contacts_deleteContact": "Delete Contact",
|
"contacts_deleteContact": "Delete Contact",
|
||||||
@@ -291,6 +458,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": {
|
||||||
@@ -302,8 +470,8 @@
|
|||||||
"contacts_filterContacts": "Filter contacts...",
|
"contacts_filterContacts": "Filter contacts...",
|
||||||
"contacts_noContactsMatchFilter": "No contacts match your filter",
|
"contacts_noContactsMatchFilter": "No contacts match your filter",
|
||||||
"contacts_noMembers": "No members",
|
"contacts_noMembers": "No members",
|
||||||
"contacts_lastSeenNow": "Last seen now",
|
"contacts_lastSeenNow": "recently",
|
||||||
"contacts_lastSeenMinsAgo": "Last seen {minutes} mins ago",
|
"contacts_lastSeenMinsAgo": "~ {minutes} min.",
|
||||||
"@contacts_lastSeenMinsAgo": {
|
"@contacts_lastSeenMinsAgo": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"minutes": {
|
"minutes": {
|
||||||
@@ -311,8 +479,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contacts_lastSeenHourAgo": "Last seen 1 hour ago",
|
"contacts_lastSeenHourAgo": "~ 1 hour",
|
||||||
"contacts_lastSeenHoursAgo": "Last seen {hours} hours ago",
|
"contacts_lastSeenHoursAgo": "~ {hours} hours",
|
||||||
"@contacts_lastSeenHoursAgo": {
|
"@contacts_lastSeenHoursAgo": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"hours": {
|
"hours": {
|
||||||
@@ -320,8 +488,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": {
|
||||||
@@ -329,6 +497,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",
|
||||||
@@ -348,6 +527,8 @@
|
|||||||
"channels_publicChannel": "Public channel",
|
"channels_publicChannel": "Public channel",
|
||||||
"channels_privateChannel": "Private channel",
|
"channels_privateChannel": "Private channel",
|
||||||
"channels_editChannel": "Edit channel",
|
"channels_editChannel": "Edit channel",
|
||||||
|
"channels_muteChannel": "Mute channel",
|
||||||
|
"channels_unmuteChannel": "Unmute channel",
|
||||||
"channels_deleteChannel": "Delete channel",
|
"channels_deleteChannel": "Delete channel",
|
||||||
"channels_deleteChannelConfirm": "Delete \"{name}\"? This cannot be undone.",
|
"channels_deleteChannelConfirm": "Delete \"{name}\"? This cannot be undone.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
@@ -357,6 +538,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"channels_channelDeleteFailed": "Failed to delete channel \"{name}\"",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"channels_channelDeleted": "Channel \"{name}\" deleted",
|
"channels_channelDeleted": "Channel \"{name}\" deleted",
|
||||||
"@channels_channelDeleted": {
|
"@channels_channelDeleted": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -418,6 +607,15 @@
|
|||||||
"channels_enterHashtag": "Enter hashtag",
|
"channels_enterHashtag": "Enter hashtag",
|
||||||
"channels_hashtagHint": "e.g. #team",
|
"channels_hashtagHint": "e.g. #team",
|
||||||
"chat_noMessages": "No messages yet",
|
"chat_noMessages": "No messages yet",
|
||||||
|
"chat_sendMessage": "Send message",
|
||||||
|
"chat_sendMessageTo": "Send message to {name}",
|
||||||
|
"@chat_sendMessageTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"chat_sendMessageToStart": "Send a message to get started",
|
"chat_sendMessageToStart": "Send a message to get started",
|
||||||
"chat_originalMessageNotFound": "Original message not found",
|
"chat_originalMessageNotFound": "Original message not found",
|
||||||
"chat_replyingTo": "Replying to {name}",
|
"chat_replyingTo": "Replying to {name}",
|
||||||
@@ -558,6 +756,7 @@
|
|||||||
},
|
},
|
||||||
"debugFrame_hexDump": "Hex Dump:",
|
"debugFrame_hexDump": "Hex Dump:",
|
||||||
"chat_pathManagement": "Path Management",
|
"chat_pathManagement": "Path Management",
|
||||||
|
"chat_ShowAllPaths": "Show all paths",
|
||||||
"chat_routingMode": "Routing mode",
|
"chat_routingMode": "Routing mode",
|
||||||
"chat_autoUseSavedPath": "Auto (use saved path)",
|
"chat_autoUseSavedPath": "Auto (use saved path)",
|
||||||
"chat_forceFloodMode": "Force Flood Mode",
|
"chat_forceFloodMode": "Force Flood Mode",
|
||||||
@@ -638,6 +837,8 @@
|
|||||||
},
|
},
|
||||||
"chat_invalidLink": "Invalid link format",
|
"chat_invalidLink": "Invalid link format",
|
||||||
"map_title": "Node Map",
|
"map_title": "Node Map",
|
||||||
|
"map_lineOfSight": "Line of Sight",
|
||||||
|
"map_losScreenTitle": "Line of Sight",
|
||||||
"map_noNodesWithLocation": "No nodes with location data",
|
"map_noNodesWithLocation": "No nodes with location data",
|
||||||
"map_nodesNeedGps": "Nodes need to share their GPS coordinates\nto appear on the map",
|
"map_nodesNeedGps": "Nodes need to share their GPS coordinates\nto appear on the map",
|
||||||
"map_nodesCount": "Nodes: {count}",
|
"map_nodesCount": "Nodes: {count}",
|
||||||
@@ -669,6 +870,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",
|
||||||
@@ -690,17 +892,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",
|
||||||
@@ -905,8 +1112,8 @@
|
|||||||
"repeater_telemetrySubtitle": "View telemetry of sensors and system stats",
|
"repeater_telemetrySubtitle": "View telemetry of sensors and system stats",
|
||||||
"repeater_cli": "CLI",
|
"repeater_cli": "CLI",
|
||||||
"repeater_cliSubtitle": "Send commands to the repeater",
|
"repeater_cliSubtitle": "Send commands to the repeater",
|
||||||
"repeater_neighbours": "Neighbors",
|
"repeater_neighbors": "Neighbors",
|
||||||
"repeater_neighboursSubtitle": "View zero hop neighbors.",
|
"repeater_neighborsSubtitle": "View zero hop neighbors.",
|
||||||
"repeater_settings": "Settings",
|
"repeater_settings": "Settings",
|
||||||
"repeater_settingsSubtitle": "Configure repeater parameters",
|
"repeater_settingsSubtitle": "Configure repeater parameters",
|
||||||
"repeater_statusTitle": "Repeater Status",
|
"repeater_statusTitle": "Repeater Status",
|
||||||
@@ -1139,6 +1346,8 @@
|
|||||||
"repeater_cliQuickVersion": "Version",
|
"repeater_cliQuickVersion": "Version",
|
||||||
"repeater_cliQuickAdvertise": "Advertise",
|
"repeater_cliQuickAdvertise": "Advertise",
|
||||||
"repeater_cliQuickClock": "Clock",
|
"repeater_cliQuickClock": "Clock",
|
||||||
|
"repeater_cliQuickClockSync": "Clock Sync",
|
||||||
|
"repeater_cliQuickDiscovery": "Discover Neighbors",
|
||||||
"repeater_cliHelpAdvert": "Sends an advertisement packet",
|
"repeater_cliHelpAdvert": "Sends an advertisement packet",
|
||||||
"repeater_cliHelpReboot": "Reboots the device. (note, you'll prob get 'Timeout' which is normal)",
|
"repeater_cliHelpReboot": "Reboots the device. (note, you'll prob get 'Timeout' which is normal)",
|
||||||
"repeater_cliHelpClock": "Displays current time per device's clock.",
|
"repeater_cliHelpClock": "Displays current time per device's clock.",
|
||||||
@@ -1266,8 +1475,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"neighbors_receivedData": "Received Neighbours Data",
|
"neighbors_receivedData": "Received Neighbors Data",
|
||||||
"neighbors_requestTimedOut": "Neighbours request timed out.",
|
"neighbors_requestTimedOut": "Neighbors request timed out.",
|
||||||
"neighbors_errorLoading": "Error loading neighbors: {error}",
|
"neighbors_errorLoading": "Error loading neighbors: {error}",
|
||||||
"@neighbors_errorLoading": {
|
"@neighbors_errorLoading": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1276,8 +1485,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"neighbors_repeatersNeighbours": "Repeaters Neighbours",
|
"neighbors_repeatersNeighbors": "Repeaters Neighbors",
|
||||||
"neighbors_noData": "No neighbours data available.",
|
"neighbors_noData": "No neighbors data available.",
|
||||||
"neighbors_unknownContact": "Unknown {pubkey}",
|
"neighbors_unknownContact": "Unknown {pubkey}",
|
||||||
"@neighbors_unknownContact": {
|
"@neighbors_unknownContact": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -1536,6 +1745,9 @@
|
|||||||
"listFilter_az": "A-Z",
|
"listFilter_az": "A-Z",
|
||||||
"listFilter_filters": "Filters",
|
"listFilter_filters": "Filters",
|
||||||
"listFilter_all": "All",
|
"listFilter_all": "All",
|
||||||
|
"listFilter_favorites": "Favorites",
|
||||||
|
"listFilter_addToFavorites": "Add to favorites",
|
||||||
|
"listFilter_removeFromFavorites": "Remove from favorites",
|
||||||
"listFilter_users": "Users",
|
"listFilter_users": "Users",
|
||||||
"listFilter_repeaters": "Repeaters",
|
"listFilter_repeaters": "Repeaters",
|
||||||
"listFilter_roomServers": "Room servers",
|
"listFilter_roomServers": "Room servers",
|
||||||
@@ -1547,6 +1759,139 @@
|
|||||||
"pathTrace_refreshTooltip": "Refresh Path Trace.",
|
"pathTrace_refreshTooltip": "Refresh Path Trace.",
|
||||||
"pathTrace_someHopsNoLocation": "One or more of the hops is missing a location!",
|
"pathTrace_someHopsNoLocation": "One or more of the hops is missing a location!",
|
||||||
"pathTrace_clearTooltip": "Clear path.",
|
"pathTrace_clearTooltip": "Clear path.",
|
||||||
|
"losSelectStartEnd": "Select start and end nodes for LOS.",
|
||||||
|
"losRunFailed": "Line-of-sight check failed: {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Clear all points",
|
||||||
|
"losRunToViewElevationProfile": "Run LOS to view elevation profile",
|
||||||
|
"losMenuTitle": "LOS Menu",
|
||||||
|
"losMenuSubtitle": "Tap nodes or long-press map for custom points",
|
||||||
|
"losShowDisplayNodes": "Show display nodes",
|
||||||
|
"losCustomPoints": "Custom points",
|
||||||
|
"losCustomPointLabel": "Custom {index}",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Point A",
|
||||||
|
"losPointB": "Point B",
|
||||||
|
"losAntennaA": "Antenna A: {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Antenna B: {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Run LOS",
|
||||||
|
"losNoElevationData": "No elevation data",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, clear LOS, min clearance {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, blocked by {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "LOS: checking...",
|
||||||
|
"losStatusNoData": "LOS: no data",
|
||||||
|
"losStatusSummary": "LOS: {clear}/{total} clear, {blocked} blocked, {unknown} unknown",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Elevation data unavailable for one or more samples.",
|
||||||
|
"losErrorInvalidInput": "Invalid points/elevation data for LOS calculation.",
|
||||||
|
"losRenameCustomPoint": "Rename custom point",
|
||||||
|
"losPointName": "Point name",
|
||||||
|
"losShowPanelTooltip": "Show LOS panel",
|
||||||
|
"losHidePanelTooltip": "Hide LOS panel",
|
||||||
|
"losElevationAttribution": "Elevation data: Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Radio horizon",
|
||||||
|
"losLegendLosBeam": "LOS beam",
|
||||||
|
"losLegendTerrain": "Terrain",
|
||||||
|
"losFrequencyLabel": "Frequency",
|
||||||
|
"losFrequencyInfoTooltip": "View calculation details",
|
||||||
|
"losFrequencyDialogTitle": "Radio horizon calculation",
|
||||||
|
"losFrequencyDialogDescription": "Starting from k={baselineK} at {baselineFreq} MHz, the calculation adjusts the k-factor for the current {frequencyMHz} MHz band, which defines the curved radio horizon cap.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"contacts_pathTrace": "Path Trace",
|
"contacts_pathTrace": "Path Trace",
|
||||||
"contacts_ping": "Ping",
|
"contacts_ping": "Ping",
|
||||||
"contacts_repeaterPathTrace": "Path trace to repeater",
|
"contacts_repeaterPathTrace": "Path trace to repeater",
|
||||||
@@ -1624,5 +1969,146 @@
|
|||||||
"settings_gpxExportChat": "Companion locations",
|
"settings_gpxExportChat": "Companion locations",
|
||||||
"settings_gpxExportAllContacts": "All contacts locations",
|
"settings_gpxExportAllContacts": "All contacts locations",
|
||||||
"settings_gpxExportShareText": "Map data exported from meshcore-open",
|
"settings_gpxExportShareText": "Map data exported from meshcore-open",
|
||||||
"settings_gpxExportShareSubject": "meshcore-open GPX map data export"
|
"settings_gpxExportShareSubject": "meshcore-open GPX map data export",
|
||||||
|
"snrIndicator_nearByRepeaters": "Nearby Repeaters",
|
||||||
|
"snrIndicator_lastSeen": "Last seen",
|
||||||
|
"contactsSettings_title": "Contacts settings",
|
||||||
|
"contactsSettings_autoAddTitle": "Automatic Discovery",
|
||||||
|
"contactsSettings_otherTitle": "Other contact related settings",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Auto-add users",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Allow the companion to automatically add discovered users.",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Auto-add repeaters",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Allow the companion to automatically add discovered repeaters.",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Auto-add room servers",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Allow the companion to automatically add discovered room servers.",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Auto-add sensors",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Allow the companion to automatically add discovered sensors.",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Overwrite Oldest",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "When the contact list is full, the oldest non-favorited contact will be replaced.",
|
||||||
|
"discoveredContacts_Title": "Discovered Contacts",
|
||||||
|
"discoveredContacts_noMatching": "No matching contacts",
|
||||||
|
"discoveredContacts_searchHint": "Search discovered contacts",
|
||||||
|
"discoveredContacts_contactAdded": "Contact added",
|
||||||
|
"discoveredContacts_addContact": "Add Contact",
|
||||||
|
"discoveredContacts_copyContact": "Copy Contact to clipboard",
|
||||||
|
"discoveredContacts_deleteContact": "Delete Discovered Contact",
|
||||||
|
"discoveredContacts_deleteContactAll": "Delete All Discovered Contacts",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Are you sure you want to delete all discovered contacts?",
|
||||||
|
"chat_sendCooldown": "Please wait a moment before sending again.",
|
||||||
|
"appSettings_jumpToOldestUnread": "Jump to oldest unread",
|
||||||
|
"appSettings_jumpToOldestUnreadSubtitle": "When opening a chat with unread messages, scroll to the first unread instead of the latest.",
|
||||||
|
"appSettings_languageHu": "Hungarian",
|
||||||
|
"appSettings_languageJa": "Japanese",
|
||||||
|
"appSettings_languageKo": "Korean",
|
||||||
|
"radioStats_tooltip": "Radio & mesh stats",
|
||||||
|
"radioStats_screenTitle": "Radio stats",
|
||||||
|
"radioStats_notConnected": "Connect to a device to view radio statistics.",
|
||||||
|
"radioStats_firmwareTooOld": "Radio statistics require companion firmware v8 or newer.",
|
||||||
|
"radioStats_waiting": "Waiting for data…",
|
||||||
|
"radioStats_noiseFloor": "Noise floor: {noiseDbm} dBm",
|
||||||
|
"@radioStats_noiseFloor": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"radioStats_lastRssi": "Last RSSI: {rssiDbm} dBm",
|
||||||
|
"@radioStats_lastRssi": {
|
||||||
|
"placeholders": {
|
||||||
|
"rssiDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"radioStats_lastSnr": "Last SNR: {snr} dB",
|
||||||
|
"@radioStats_lastSnr": {
|
||||||
|
"placeholders": {
|
||||||
|
"snr": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"radioStats_txAir": "TX airtime (total): {seconds} s",
|
||||||
|
"@radioStats_txAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"radioStats_rxAir": "RX airtime (total): {seconds} s",
|
||||||
|
"@radioStats_rxAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"radioStats_chartCaption": "Noise floor (dBm) over recent samples.",
|
||||||
|
"radioStats_stripNoise": "Noise floor: {noiseDbm} dBm",
|
||||||
|
"@radioStats_stripNoise": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"radioStats_stripWaiting": "Fetching radio stats…",
|
||||||
|
"radioStats_settingsTile": "Radio stats",
|
||||||
|
"radioStats_settingsSubtitle": "Noise floor, RSSI, SNR, and airtime",
|
||||||
|
|
||||||
|
"translation_title": "Translation",
|
||||||
|
"translation_enableTitle": "Enable translation",
|
||||||
|
"translation_enableSubtitle": "Translate incoming messages and allow pre-send translation.",
|
||||||
|
"translation_composerTitle": "Translate before sending",
|
||||||
|
"translation_composerSubtitle": "Controls the default state of the composer translation icon.",
|
||||||
|
"translation_targetLanguage": "Target language",
|
||||||
|
"translation_useAppLanguage": "Use app language",
|
||||||
|
"translation_downloadedModelLabel": "Downloaded model",
|
||||||
|
"translation_presetModelLabel": "Preset Hugging Face model",
|
||||||
|
"translation_manualUrlLabel": "Manual model URL",
|
||||||
|
"translation_downloadModel": "Download model",
|
||||||
|
"translation_downloading": "Downloading...",
|
||||||
|
"translation_working": "Working...",
|
||||||
|
"translation_stop": "Stop",
|
||||||
|
"translation_mergingChunks": "Merging downloaded chunks into final file...",
|
||||||
|
"translation_downloadedModels": "Downloaded models",
|
||||||
|
"translation_deleteModel": "Delete model",
|
||||||
|
"translation_modelDownloaded": "Translation model downloaded.",
|
||||||
|
"translation_downloadStopped": "Download stopped.",
|
||||||
|
"translation_downloadFailed": "Download failed: {error}",
|
||||||
|
"@translation_downloadFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_enterUrlFirst": "Enter a model URL first.",
|
||||||
|
"scanner_linuxPairingShowPin": "Show PIN",
|
||||||
|
"scanner_linuxPairingHidePin": "Hide PIN",
|
||||||
|
"scanner_linuxPairingPinTitle": "Bluetooth Pairing PIN",
|
||||||
|
"scanner_linuxPairingPinPrompt": "Enter PIN for {deviceName} (leave blank if none).",
|
||||||
|
"@scanner_linuxPairingPinPrompt": {
|
||||||
|
"placeholders": {
|
||||||
|
"deviceName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_messageTranslation": "Message translation",
|
||||||
|
"translation_translateBeforeSending": "Translate before sending",
|
||||||
|
"translation_composerEnabledHint": "Messages will be translated before send.",
|
||||||
|
"translation_composerDisabledHint": "Send messages in the original typed language.",
|
||||||
|
"translation_translateTo": "Translate to {language}",
|
||||||
|
"@translation_translateTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"language": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_translationOptions": "Translation options",
|
||||||
|
"translation_systemLanguage": "System language"
|
||||||
}
|
}
|
||||||
|
|||||||
+474
-9
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"channels_channelDeleteFailed": "No se pudo eliminar el canal \"{name}\"",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@@locale": "es",
|
"@@locale": "es",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Contactos",
|
"nav_contacts": "Contactos",
|
||||||
@@ -277,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": {
|
||||||
@@ -289,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": {
|
||||||
@@ -297,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": {
|
||||||
@@ -306,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": {
|
||||||
@@ -334,6 +343,8 @@
|
|||||||
"channels_publicChannel": "Canal público",
|
"channels_publicChannel": "Canal público",
|
||||||
"channels_privateChannel": "Canal privado",
|
"channels_privateChannel": "Canal privado",
|
||||||
"channels_editChannel": "Editar canal",
|
"channels_editChannel": "Editar canal",
|
||||||
|
"channels_muteChannel": "Silenciar canal",
|
||||||
|
"channels_unmuteChannel": "Activar canal",
|
||||||
"channels_deleteChannel": "Eliminar canal",
|
"channels_deleteChannel": "Eliminar canal",
|
||||||
"channels_deleteChannelConfirm": "Eliminar \"{name}\"? Esto no se puede deshacer.",
|
"channels_deleteChannelConfirm": "Eliminar \"{name}\"? Esto no se puede deshacer.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
@@ -1351,12 +1362,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Vecinos",
|
"repeater_neighbors": "Vecinos",
|
||||||
"repeater_neighboursSubtitle": "Ver vecinos de salto cero.",
|
"repeater_neighborsSubtitle": "Ver vecinos de salto cero.",
|
||||||
"neighbors_receivedData": "Recibidas Datos de Vecinos",
|
"neighbors_receivedData": "Recibidas Datos de Vecinos",
|
||||||
"neighbors_requestTimedOut": "Los vecinos solicitan que se desconecte.",
|
"neighbors_requestTimedOut": "Los vecinos solicitan que se desconecte.",
|
||||||
"neighbors_errorLoading": "Error al cargar vecinos: {error}",
|
"neighbors_errorLoading": "Error al cargar vecinos: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Repetidores Vecinos",
|
"neighbors_repeatersNeighbors": "Repetidores Vecinos",
|
||||||
"neighbors_noData": "No hay datos de vecinos disponibles.",
|
"neighbors_noData": "No hay datos de vecinos disponibles.",
|
||||||
"channels_joinPrivateChannel": "Únete a un Canal Privado",
|
"channels_joinPrivateChannel": "Únete a un Canal Privado",
|
||||||
"channels_createPrivateChannel": "Crear un Canal Privado",
|
"channels_createPrivateChannel": "Crear un Canal Privado",
|
||||||
@@ -1551,6 +1562,8 @@
|
|||||||
"appSettings_languageUk": "Ucraniano",
|
"appSettings_languageUk": "Ucraniano",
|
||||||
"contacts_clipboardEmpty": "El portapapeles está vacío.",
|
"contacts_clipboardEmpty": "El portapapeles está vacío.",
|
||||||
"appSettings_languageRu": "Ruso",
|
"appSettings_languageRu": "Ruso",
|
||||||
|
"appSettings_enableMessageTracing": "Habilitar seguimiento de mensajes",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Mostrar metadatos detallados de enrutamiento y tiempo para los mensajes",
|
||||||
"contacts_invalidAdvertFormat": "Datos de contacto no válidos",
|
"contacts_invalidAdvertFormat": "Datos de contacto no válidos",
|
||||||
"contacts_floodAdvert": "Anuncio de inundación",
|
"contacts_floodAdvert": "Anuncio de inundación",
|
||||||
"contacts_contactImported": "El contacto ha sido importado.",
|
"contacts_contactImported": "El contacto ha sido importado.",
|
||||||
@@ -1620,9 +1633,461 @@
|
|||||||
"map_removeLast": "Eliminar último",
|
"map_removeLast": "Eliminar último",
|
||||||
"map_pathTraceCancelled": "Rastreo de ruta cancelado.",
|
"map_pathTraceCancelled": "Rastreo de ruta cancelado.",
|
||||||
"scanner_bluetoothOffMessage": "Por favor, active el Bluetooth para escanear dispositivos.",
|
"scanner_bluetoothOffMessage": "Por favor, active el Bluetooth para escanear dispositivos.",
|
||||||
|
"scanner_chromeRequired": "Navegador Chrome requerido",
|
||||||
|
"scanner_chromeRequiredMessage": "Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.",
|
||||||
"scanner_bluetoothOff": "Bluetooth está desactivado.",
|
"scanner_bluetoothOff": "Bluetooth está desactivado.",
|
||||||
"scanner_enableBluetooth": "Habilitar Bluetooth",
|
"scanner_enableBluetooth": "Habilitar Bluetooth",
|
||||||
|
"snrIndicator_nearByRepeaters": "Repetidores cercanos",
|
||||||
|
"snrIndicator_lastSeen": "Visto por última vez",
|
||||||
|
"chat_ShowAllPaths": "Mostrar todos los caminos",
|
||||||
"settings_clientRepeatFreqWarning": "Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.",
|
"settings_clientRepeatFreqWarning": "Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.",
|
||||||
"settings_clientRepeat": "Repetir sin conexión",
|
"settings_clientRepeat": "Repetir sin conexión",
|
||||||
"settings_clientRepeatSubtitle": "Permita que este dispositivo repita los paquetes de red para otros usuarios."
|
"settings_clientRepeatSubtitle": "Permita que este dispositivo repita los paquetes de red para otros usuarios.",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "Datos de elevación LOS: Open-Meteo (CC BY 4.0)",
|
||||||
|
"appSettings_unitsTitle": "Unidades",
|
||||||
|
"appSettings_unitsMetric": "Métrico (m/km)",
|
||||||
|
"appSettings_unitsImperial": "Imperial (pies/millas)",
|
||||||
|
"map_lineOfSight": "Línea de visión",
|
||||||
|
"map_losScreenTitle": "Línea de visión",
|
||||||
|
"losSelectStartEnd": "Seleccione los nodos de inicio y fin para LOS.",
|
||||||
|
"losRunFailed": "Error en la comprobación de la línea de visión: {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Borrar todos los puntos",
|
||||||
|
"losRunToViewElevationProfile": "Ejecute LOS para ver el perfil de elevación",
|
||||||
|
"losMenuTitle": "Menú LOS",
|
||||||
|
"losMenuSubtitle": "Toque nodos o mantenga presionado el mapa para puntos personalizados",
|
||||||
|
"losShowDisplayNodes": "Mostrar nodos de visualización",
|
||||||
|
"losCustomPoints": "Puntos personalizados",
|
||||||
|
"losCustomPointLabel": "Personalizado {index}",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Punto A",
|
||||||
|
"losPointB": "Punto B",
|
||||||
|
"losAntennaA": "Antena A: {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Antena B: {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Ejecutar LOS",
|
||||||
|
"losNoElevationData": "Sin datos de elevación",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, despejar LOS, autorización mínima {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, bloqueado por {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "LOS: comprobando...",
|
||||||
|
"losStatusNoData": "LOS: sin datos",
|
||||||
|
"losStatusSummary": "LOS: {clear}/{total} claro, {blocked} bloqueado, {unknown} desconocido",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Datos de elevación no disponibles para una o más muestras.",
|
||||||
|
"losErrorInvalidInput": "Datos de puntos/elevación no válidos para el cálculo de LOS.",
|
||||||
|
"losRenameCustomPoint": "Cambiar el nombre del punto personalizado",
|
||||||
|
"losPointName": "Nombre del punto",
|
||||||
|
"losShowPanelTooltip": "Mostrar panel LOS",
|
||||||
|
"losHidePanelTooltip": "Ocultar panel LOS",
|
||||||
|
"losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Horizonte radioeléctrico",
|
||||||
|
"losLegendLosBeam": "Línea de visión",
|
||||||
|
"losLegendTerrain": "Terreno",
|
||||||
|
"losFrequencyLabel": "Frecuencia",
|
||||||
|
"losFrequencyInfoTooltip": "Ver detalles del cálculo",
|
||||||
|
"losFrequencyDialogTitle": "Cálculo del horizonte radioeléctrico",
|
||||||
|
"losFrequencyDialogDescription": "A partir de k={baselineK} en {baselineFreq} MHz, el cálculo ajusta el factor k para la banda actual de {frequencyMHz} MHz, que define el límite curvo del horizonte de radio.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listFilter_favorites": "Favoritos",
|
||||||
|
"listFilter_removeFromFavorites": "Eliminar de las favoritas",
|
||||||
|
"listFilter_addToFavorites": "Añadir a favoritos",
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_searchContactsNoNumber": "Buscar contactos...",
|
||||||
|
"contacts_unread": "No leído",
|
||||||
|
"contacts_searchFavorites": "Buscar {number}{str} Favoritos...",
|
||||||
|
"contacts_searchUsers": "Buscar {number}{str} Usuarios...",
|
||||||
|
"contacts_searchRepeaters": "Buscar {number}{str} Repetidores...",
|
||||||
|
"contacts_searchRoomServers": "Buscar {number}{str} servidores de sala...",
|
||||||
|
"contactsSettings_autoAddTitle": "Detección automática",
|
||||||
|
"settings_contactSettings": "Configuración de contacto",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Agregar usuarios automáticamente",
|
||||||
|
"contactsSettings_otherTitle": "Otras configuraciones relacionadas con el contacto",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Permitir que el compañero agregue automáticamente a los usuarios descubiertos.",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Permitir que el compañero agregue automáticamente los repetidores descubiertos.",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Permitir que el compañero agregue automáticamente los servidores de salas descubiertos.",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Agregar sensores automáticamente",
|
||||||
|
"contactsSettings_title": "Configuración de contactos",
|
||||||
|
"settings_contactSettingsSubtitle": "Configuración de cómo se agregan los contactos.",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Permitir que el compañero agregue automáticamente los sensores descubiertos.",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Agregar repetidores automáticamente",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Sobreescribir el más antiguo",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Agregar automáticamente servidores de sala",
|
||||||
|
"discoveredContacts_noMatching": "No se encontraron contactos coincidentes",
|
||||||
|
"discoveredContacts_contactAdded": "Contacto agregado",
|
||||||
|
"discoveredContacts_copyContact": "Copiar contacto al portapapeles",
|
||||||
|
"discoveredContacts_deleteContact": "Eliminar contacto",
|
||||||
|
"discoveredContacts_Title": "Contactos descubiertos",
|
||||||
|
"discoveredContacts_searchHint": "Buscar contactos descubiertos",
|
||||||
|
"discoveredContacts_addContact": "Agregar contacto",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Cuando la lista de contactos esté llena, se reemplazará el contacto no favorito más antiguo.",
|
||||||
|
"common_deleteAll": "Eliminar todo",
|
||||||
|
"discoveredContacts_deleteContactAll": "Eliminar Todos los Contactos Descubiertos",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "¿Está seguro de que desea eliminar todos los contactos descubiertos!",
|
||||||
|
"map_guessedLocation": "Ubicación estimada",
|
||||||
|
"map_showGuessedLocations": "Mostrar las ubicaciones estimadas de los nodos.",
|
||||||
|
"usbScreenTitle": "Conecte mediante USB",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenSubtitle": "Seleccione el dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.",
|
||||||
|
"usbScreenStatus": "Seleccione un dispositivo USB",
|
||||||
|
"usbScreenNote": "La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.",
|
||||||
|
"usbScreenEmptyState": "No se encontraron dispositivos USB. Conecte uno y vuelva a intentar.",
|
||||||
|
"usbErrorPermissionDenied": "Se denegó el permiso de acceso a través de USB.",
|
||||||
|
"usbErrorDeviceMissing": "El dispositivo USB seleccionado ya no está disponible.",
|
||||||
|
"usbErrorInvalidPort": "Seleccione un dispositivo USB válido.",
|
||||||
|
"usbErrorBusy": "Ya se ha iniciado una solicitud de conexión USB adicional.",
|
||||||
|
"usbErrorNotConnected": "No hay ningún dispositivo USB conectado.",
|
||||||
|
"usbErrorOpenFailed": "No se pudo abrir el dispositivo USB seleccionado.",
|
||||||
|
"usbErrorConnectFailed": "No se pudo conectar con el dispositivo USB seleccionado.",
|
||||||
|
"usbErrorUnsupported": "La comunicación serial a través de USB no está soportada en esta plataforma.",
|
||||||
|
"usbErrorAlreadyActive": "La conexión USB ya está activa.",
|
||||||
|
"usbErrorNoDeviceSelected": "No se ha seleccionado ningún dispositivo USB.",
|
||||||
|
"usbErrorPortClosed": "La conexión USB no está activa.",
|
||||||
|
"usbFallbackDeviceName": "Dispositivo de serie web",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_connecting": "Conectándose al dispositivo USB...",
|
||||||
|
"usbStatus_searching": "Buscando dispositivos USB...",
|
||||||
|
"usbStatus_notConnected": "Seleccione un dispositivo USB",
|
||||||
|
"usbConnectionFailed": "Error al conectar mediante USB: {error}",
|
||||||
|
"usbErrorConnectTimedOut": "La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion.",
|
||||||
|
"@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.",
|
||||||
|
"@radioStats_noiseFloor": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_lastRssi": {
|
||||||
|
"placeholders": {
|
||||||
|
"rssiDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_lastSnr": {
|
||||||
|
"placeholders": {
|
||||||
|
"snr": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_txAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_rxAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_stripNoise": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"appSettings_jumpToOldestUnread": "Salta a los mensajes más antiguos sin leer",
|
||||||
|
"chat_sendCooldown": "Por favor, espere un momento antes de reenviar.",
|
||||||
|
"appSettings_languageHu": "Húngaro",
|
||||||
|
"appSettings_jumpToOldestUnreadSubtitle": "Cuando abras una conversación con mensajes sin leer, desplázate hacia el primer mensaje sin leer en lugar del más reciente.",
|
||||||
|
"appSettings_languageJa": "Japonés",
|
||||||
|
"appSettings_languageKo": "Coreano",
|
||||||
|
"radioStats_tooltip": "Estadísticas de radio y malla",
|
||||||
|
"radioStats_screenTitle": "Estadísticas de radio",
|
||||||
|
"radioStats_notConnected": "Conéctese a un dispositivo para visualizar estadísticas de radio.",
|
||||||
|
"radioStats_firmwareTooOld": "Las estadísticas de radio requieren un firmware compatible v8 o posterior.",
|
||||||
|
"radioStats_waiting": "Esperando datos…",
|
||||||
|
"radioStats_noiseFloor": "Nivel de ruido: {noiseDbm} dBm",
|
||||||
|
"radioStats_lastRssi": "Último RSSI: {rssiDbm} dBm",
|
||||||
|
"radioStats_lastSnr": "Último SNR: {snr} dB",
|
||||||
|
"radioStats_txAir": "Tiempo de emisión en Texas (total): {seconds} s",
|
||||||
|
"radioStats_rxAir": "Tiempo de transmisión de RX (total): {seconds} s",
|
||||||
|
"radioStats_chartCaption": "Nivel de ruido (dBm) en muestras recientes.",
|
||||||
|
"radioStats_stripNoise": "Nivel de ruido: {noiseDbm} dBm",
|
||||||
|
"radioStats_stripWaiting": "Obteniendo estadísticas de la radio…",
|
||||||
|
"radioStats_settingsTile": "Estadísticas de radio",
|
||||||
|
"radioStats_settingsSubtitle": "Nivel de ruido, RSSI, SNR y tiempo de transmisión",
|
||||||
|
"@translation_downloadFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_title": "Traducción",
|
||||||
|
"translation_enableSubtitle": "Traducir los mensajes entrantes y permitir la traducción previa al envío.",
|
||||||
|
"translation_enableTitle": "Habilitar la traducción",
|
||||||
|
"translation_composerTitle": "Traducir antes de enviar",
|
||||||
|
"translation_composerSubtitle": "Controla el estado predeterminado del icono de traducción del compositor.",
|
||||||
|
"translation_targetLanguage": "Idioma de destino",
|
||||||
|
"translation_useAppLanguage": "Utilizar el idioma de la aplicación",
|
||||||
|
"translation_downloadedModelLabel": "Modelo descargado",
|
||||||
|
"translation_presetModelLabel": "Modelo predefinido de Hugging Face",
|
||||||
|
"translation_manualUrlLabel": "URL del modelo manual",
|
||||||
|
"translation_downloadModel": "Descargar el modelo",
|
||||||
|
"translation_downloading": "Descargando...",
|
||||||
|
"translation_working": "Trabajando...",
|
||||||
|
"translation_stop": "¡Detente!",
|
||||||
|
"translation_mergingChunks": "Combinando los fragmentos descargados en el archivo final...",
|
||||||
|
"translation_downloadedModels": "Modelos descargados",
|
||||||
|
"translation_deleteModel": "Eliminar modelo",
|
||||||
|
"translation_modelDownloaded": "Modelo de traducción descargado.",
|
||||||
|
"translation_downloadStopped": "La descarga se ha detenido.",
|
||||||
|
"translation_downloadFailed": "No se pudo descargar: {error}",
|
||||||
|
"translation_enterUrlFirst": "Primero, introduzca la URL del modelo.",
|
||||||
|
"@scanner_linuxPairingPinPrompt": {
|
||||||
|
"placeholders": {
|
||||||
|
"deviceName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scanner_linuxPairingPinPrompt": "Introduzca el código PIN para {deviceName} (deje en blanco si no hay ninguno).",
|
||||||
|
"scanner_linuxPairingShowPin": "Mostrar código PIN",
|
||||||
|
"scanner_linuxPairingPinTitle": "PIN para emparejar dispositivos Bluetooth",
|
||||||
|
"scanner_linuxPairingHidePin": "Ocultar PIN",
|
||||||
|
"@translation_translateTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"language": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_composerDisabledHint": "Envía mensajes utilizando el lenguaje escrito original.",
|
||||||
|
"translation_composerEnabledHint": "Los mensajes serán traducidos antes de ser enviados.",
|
||||||
|
"translation_messageTranslation": "Traducción del mensaje",
|
||||||
|
"translation_translateBeforeSending": "Traducir antes de enviar",
|
||||||
|
"translation_translateTo": "Traducir a {language}",
|
||||||
|
"translation_translationOptions": "Opciones de traducción",
|
||||||
|
"translation_systemLanguage": "Idioma del sistema",
|
||||||
|
"repeater_cliQuickDiscovery": "Descubrir Vecinos",
|
||||||
|
"repeater_cliQuickClockSync": "Sincronización del reloj"
|
||||||
}
|
}
|
||||||
+497
-32
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"channels_channelDeleteFailed": "Échec de la suppression de la chaîne \"{name}\"",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@@locale": "fr",
|
"@@locale": "fr",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Contacts",
|
"nav_contacts": "Contacts",
|
||||||
@@ -135,8 +143,8 @@
|
|||||||
"settings_frequencyHelper": "300,0 - 2 500,0",
|
"settings_frequencyHelper": "300,0 - 2 500,0",
|
||||||
"settings_frequencyInvalid": "Fréquence invalide (300-2500 MHz)",
|
"settings_frequencyInvalid": "Fréquence invalide (300-2500 MHz)",
|
||||||
"settings_bandwidth": "Bande passante",
|
"settings_bandwidth": "Bande passante",
|
||||||
"settings_spreadingFactor": "Facteur de répartition",
|
"settings_spreadingFactor": "Facteur de répartition (SF)",
|
||||||
"settings_codingRate": "Taux de codage",
|
"settings_codingRate": "Taux de codage (CR)",
|
||||||
"settings_txPower": "TX Puissance (dBm)",
|
"settings_txPower": "TX Puissance (dBm)",
|
||||||
"settings_txPowerHelper": "0 - 22",
|
"settings_txPowerHelper": "0 - 22",
|
||||||
"settings_txPowerInvalid": "Puissance TX invalide (0-22 dBm)",
|
"settings_txPowerInvalid": "Puissance TX invalide (0-22 dBm)",
|
||||||
@@ -262,7 +270,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contacts_manageRepeater": "Gérer le répéteur",
|
"contacts_manageRepeater": "Gérer le répéteur",
|
||||||
"contacts_roomLogin": "Connexion Salle",
|
"contacts_roomLogin": "Connexion Room Server",
|
||||||
"contacts_openChat": "Ouverture du Chat",
|
"contacts_openChat": "Ouverture du Chat",
|
||||||
"contacts_editGroup": "Modifier le groupe",
|
"contacts_editGroup": "Modifier le groupe",
|
||||||
"contacts_deleteGroup": "Supprimer le groupe",
|
"contacts_deleteGroup": "Supprimer le groupe",
|
||||||
@@ -277,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": {
|
||||||
@@ -289,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": {
|
||||||
@@ -297,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": {
|
||||||
@@ -306,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": {
|
||||||
@@ -334,6 +343,8 @@
|
|||||||
"channels_publicChannel": "Canal public",
|
"channels_publicChannel": "Canal public",
|
||||||
"channels_privateChannel": "Canal privé",
|
"channels_privateChannel": "Canal privé",
|
||||||
"channels_editChannel": "Modifier le canal",
|
"channels_editChannel": "Modifier le canal",
|
||||||
|
"channels_muteChannel": "Désactiver les notifications du canal",
|
||||||
|
"channels_unmuteChannel": "Réactiver les notifications du canal",
|
||||||
"channels_deleteChannel": "Supprimer le canal",
|
"channels_deleteChannel": "Supprimer le canal",
|
||||||
"channels_deleteChannelConfirm": "Supprimer {name}? Cela ne peut pas être annulé.",
|
"channels_deleteChannelConfirm": "Supprimer {name}? Cela ne peut pas être annulé.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
@@ -556,7 +567,7 @@
|
|||||||
"chat_clearPath": "Effacer le chemin",
|
"chat_clearPath": "Effacer le chemin",
|
||||||
"chat_clearPathSubtitle": "Forcer la redécouverte lors de la prochaine envoi",
|
"chat_clearPathSubtitle": "Forcer la redécouverte lors de la prochaine envoi",
|
||||||
"chat_pathCleared": "Le chemin est dégagé. Le prochain message redécouvrira le tracé.",
|
"chat_pathCleared": "Le chemin est dégagé. Le prochain message redécouvrira le tracé.",
|
||||||
"chat_floodModeSubtitle": "Utiliser le commutateur de routage dans la barre d'application",
|
"chat_floodModeSubtitle": "Désactive l'apprentissage du chemin (à éviter). Utiliser le commutateur de routage dans la barre d'application pour rebasculer en mode auto par la suite.",
|
||||||
"chat_floodModeEnabled": "Le mode envoi à tout le réseau est activé. Changer via l'icône de routage dans la barre d'outils.",
|
"chat_floodModeEnabled": "Le mode envoi à tout le réseau est activé. Changer via l'icône de routage dans la barre d'outils.",
|
||||||
"chat_fullPath": "Chemin complet",
|
"chat_fullPath": "Chemin complet",
|
||||||
"chat_pathDetailsNotAvailable": "Les détails du chemin ne sont pas encore disponibles. Essayez d'envoyer un message pour rafraîchir.",
|
"chat_pathDetailsNotAvailable": "Les détails du chemin ne sont pas encore disponibles. Essayez d'envoyer un message pour rafraîchir.",
|
||||||
@@ -632,7 +643,7 @@
|
|||||||
},
|
},
|
||||||
"map_chat": "Chat",
|
"map_chat": "Chat",
|
||||||
"map_repeater": "Répéteur",
|
"map_repeater": "Répéteur",
|
||||||
"map_room": "Salle",
|
"map_room": "Room Server",
|
||||||
"map_sensor": "Capteur",
|
"map_sensor": "Capteur",
|
||||||
"map_pinDm": "Clé (DM)",
|
"map_pinDm": "Clé (DM)",
|
||||||
"map_pinPrivate": "Verrouiller (Privé)",
|
"map_pinPrivate": "Verrouiller (Privé)",
|
||||||
@@ -671,7 +682,7 @@
|
|||||||
"map_showSharedMarkers": "Afficher les marqueurs partagés",
|
"map_showSharedMarkers": "Afficher les marqueurs partagés",
|
||||||
"map_lastSeenTime": "Dernière fois vu",
|
"map_lastSeenTime": "Dernière fois vu",
|
||||||
"map_sharedPin": "Clé partagée",
|
"map_sharedPin": "Clé partagée",
|
||||||
"map_joinRoom": "Rejoindre la salle",
|
"map_joinRoom": "Rejoindre le room server",
|
||||||
"map_manageRepeater": "Gérer le répéteur",
|
"map_manageRepeater": "Gérer le répéteur",
|
||||||
"mapCache_title": "Cache de Carte Hors Ligne",
|
"mapCache_title": "Cache de Carte Hors Ligne",
|
||||||
"mapCache_selectAreaFirst": "Sélectionner une zone pour la mise en cache en premier",
|
"mapCache_selectAreaFirst": "Sélectionner une zone pour la mise en cache en premier",
|
||||||
@@ -796,7 +807,7 @@
|
|||||||
"dialog_disconnect": "Déconnecter",
|
"dialog_disconnect": "Déconnecter",
|
||||||
"dialog_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?",
|
"dialog_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?",
|
||||||
"login_repeaterLogin": "Connexion au répéteur",
|
"login_repeaterLogin": "Connexion au répéteur",
|
||||||
"login_roomLogin": "Connexion Salle",
|
"login_roomLogin": "Connexion Room Server",
|
||||||
"login_password": "Mot de passe",
|
"login_password": "Mot de passe",
|
||||||
"login_enterPassword": "Entrez votre mot de passe",
|
"login_enterPassword": "Entrez votre mot de passe",
|
||||||
"login_savePassword": "Sauvegarder le mot de passe",
|
"login_savePassword": "Sauvegarder le mot de passe",
|
||||||
@@ -854,7 +865,7 @@
|
|||||||
"path_labelHexPrefixes": "Préfixes hexadécimaux",
|
"path_labelHexPrefixes": "Préfixes hexadécimaux",
|
||||||
"path_helperMaxHops": "Max 64 sauts. Chaque préfixe fait 2 caractères hexadécimaux (1 octet)",
|
"path_helperMaxHops": "Max 64 sauts. Chaque préfixe fait 2 caractères hexadécimaux (1 octet)",
|
||||||
"path_selectFromContacts": "Sélectionner à partir des contacts :",
|
"path_selectFromContacts": "Sélectionner à partir des contacts :",
|
||||||
"path_noRepeatersFound": "Aucun répéteur ou serveur de salle n'a été trouvé.",
|
"path_noRepeatersFound": "Aucun répéteur ou room server n'a été trouvé.",
|
||||||
"path_customPathsRequire": "Les chemins personnalisés nécessitent des sauts intermédiaires qui peuvent transmettre des messages.",
|
"path_customPathsRequire": "Les chemins personnalisés nécessitent des sauts intermédiaires qui peuvent transmettre des messages.",
|
||||||
"path_invalidHexPrefixes": "Préfixes hexadécimaux invalides : {prefixes}",
|
"path_invalidHexPrefixes": "Préfixes hexadécimaux invalides : {prefixes}",
|
||||||
"@path_invalidHexPrefixes": {
|
"@path_invalidHexPrefixes": {
|
||||||
@@ -985,15 +996,15 @@
|
|||||||
"repeater_txPower": "TX Puissance",
|
"repeater_txPower": "TX Puissance",
|
||||||
"repeater_txPowerHelper": "1-30 dBm",
|
"repeater_txPowerHelper": "1-30 dBm",
|
||||||
"repeater_bandwidth": "Bande passante",
|
"repeater_bandwidth": "Bande passante",
|
||||||
"repeater_spreadingFactor": "Facteur de répartition",
|
"repeater_spreadingFactor": "Facteur de répartition (SF)",
|
||||||
"repeater_codingRate": "Taux de codage",
|
"repeater_codingRate": "Taux de codage (CR)",
|
||||||
"repeater_locationSettings": "Paramètres de localisation",
|
"repeater_locationSettings": "Paramètres de localisation",
|
||||||
"repeater_latitude": "Latitude",
|
"repeater_latitude": "Latitude",
|
||||||
"repeater_latitudeHelper": "Degrés décimaux (par exemple, 37.7749)",
|
"repeater_latitudeHelper": "Degrés décimaux (par exemple, 37.7749)",
|
||||||
"repeater_longitude": "Longitude",
|
"repeater_longitude": "Longitude",
|
||||||
"repeater_longitudeHelper": "Degrés décimaux (par exemple, -122,4194)",
|
"repeater_longitudeHelper": "Degrés décimaux (par exemple, -122,4194)",
|
||||||
"repeater_features": "Fonctionnalités",
|
"repeater_features": "Fonctionnalités",
|
||||||
"repeater_packetForwarding": "Transfert de paquets",
|
"repeater_packetForwarding": "Mode répéteur",
|
||||||
"repeater_packetForwardingSubtitle": "Activer le répéteur pour transmettre des paquets",
|
"repeater_packetForwardingSubtitle": "Activer le répéteur pour transmettre des paquets",
|
||||||
"repeater_guestAccess": "Accès Invité",
|
"repeater_guestAccess": "Accès Invité",
|
||||||
"repeater_guestAccessSubtitle": "Autoriser l'accès invité en lecture seule",
|
"repeater_guestAccessSubtitle": "Autoriser l'accès invité en lecture seule",
|
||||||
@@ -1351,12 +1362,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Voisins",
|
"repeater_neighbors": "Voisins",
|
||||||
"repeater_neighboursSubtitle": "Afficher les voisins de saut nuls.",
|
"repeater_neighborsSubtitle": "Afficher les voisins de saut nuls.",
|
||||||
"neighbors_receivedData": "Données des voisins reçues",
|
"neighbors_receivedData": "Données des voisins reçues",
|
||||||
"neighbors_requestTimedOut": "Les voisins demandent un délai.",
|
"neighbors_requestTimedOut": "Les voisins demandent un délai.",
|
||||||
"neighbors_errorLoading": "Erreur lors du chargement des voisins : {error}",
|
"neighbors_errorLoading": "Erreur lors du chargement des voisins : {error}",
|
||||||
"neighbors_repeatersNeighbours": "Répéteurs Voisins",
|
"neighbors_repeatersNeighbors": "Répéteurs Voisins",
|
||||||
"neighbors_noData": "Aucune donnée concernant les voisins disponible.",
|
"neighbors_noData": "Aucune donnée concernant les voisins disponible.",
|
||||||
"channels_createPrivateChannelDesc": "Sécurisé avec une clé secrète.",
|
"channels_createPrivateChannelDesc": "Sécurisé avec une clé secrète.",
|
||||||
"channels_joinPrivateChannel": "Rejoindre un Canal Privé",
|
"channels_joinPrivateChannel": "Rejoindre un Canal Privé",
|
||||||
@@ -1366,7 +1377,7 @@
|
|||||||
"channels_joinPublicChannelDesc": "Tout le monde peut rejoindre ce canal.",
|
"channels_joinPublicChannelDesc": "Tout le monde peut rejoindre ce canal.",
|
||||||
"channels_joinHashtagChannel": "Rejoindre un Canal Hashtag",
|
"channels_joinHashtagChannel": "Rejoindre un Canal Hashtag",
|
||||||
"channels_joinHashtagChannelDesc": "N'importe qui peut rejoindre les canaux #hashtag.",
|
"channels_joinHashtagChannelDesc": "N'importe qui peut rejoindre les canaux #hashtag.",
|
||||||
"channels_scanQrCode": "Scanner un code QR",
|
"channels_scanQrCode": "Scanner un QR code",
|
||||||
"channels_scanQrCodeComingSoon": "Bientôt disponible",
|
"channels_scanQrCodeComingSoon": "Bientôt disponible",
|
||||||
"channels_enterHashtag": "Entrez le hashtag",
|
"channels_enterHashtag": "Entrez le hashtag",
|
||||||
"channels_hashtagHint": "ex. #equipe",
|
"channels_hashtagHint": "ex. #equipe",
|
||||||
@@ -1391,7 +1402,7 @@
|
|||||||
"settings_locationIntervalSec": "Intervalle de mise-à-jour du GPS (Secondes)",
|
"settings_locationIntervalSec": "Intervalle de mise-à-jour du GPS (Secondes)",
|
||||||
"settings_locationIntervalInvalid": "L'intervalle doit être compris entre 60 et 86400 secondes.",
|
"settings_locationIntervalInvalid": "L'intervalle doit être compris entre 60 et 86400 secondes.",
|
||||||
"contacts_manageRoom": "Gérer le Room Server",
|
"contacts_manageRoom": "Gérer le Room Server",
|
||||||
"room_management": "Administración del Servidor de Habitación",
|
"room_management": "Administrattion Room Server",
|
||||||
"@community_joinConfirmation": {
|
"@community_joinConfirmation": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"name": {
|
"name": {
|
||||||
@@ -1455,8 +1466,8 @@
|
|||||||
"community_join": "Rejoindre",
|
"community_join": "Rejoindre",
|
||||||
"community_joinTitle": "Rejoindre la communauté",
|
"community_joinTitle": "Rejoindre la communauté",
|
||||||
"community_joinConfirmation": "Souhaitez-vous rejoindre la communauté \"{name}\" ?",
|
"community_joinConfirmation": "Souhaitez-vous rejoindre la communauté \"{name}\" ?",
|
||||||
"community_scanQr": "Scanner la communauté QR",
|
"community_scanQr": "Scanner un QR code de communauté",
|
||||||
"community_scanInstructions": "Pointez l'appareil photo vers un code QR communautaire.",
|
"community_scanInstructions": "Pointez l'appareil photo vers un QR code de communauté.",
|
||||||
"community_showQr": "Afficher le QR Code",
|
"community_showQr": "Afficher le QR Code",
|
||||||
"community_publicChannel": "Communauté Publique",
|
"community_publicChannel": "Communauté Publique",
|
||||||
"community_hashtagChannel": "Hashtag Communauté",
|
"community_hashtagChannel": "Hashtag Communauté",
|
||||||
@@ -1467,13 +1478,13 @@
|
|||||||
"community_qrTitle": "Partager Communauté",
|
"community_qrTitle": "Partager Communauté",
|
||||||
"community_qrInstructions": "Scanner ce QR code pour rejoindre {name}",
|
"community_qrInstructions": "Scanner ce QR code pour rejoindre {name}",
|
||||||
"community_hashtagPrivacyHint": "Les canaux hashtag de la communauté ne sont accessibles qu'aux membres de la communauté",
|
"community_hashtagPrivacyHint": "Les canaux hashtag de la communauté ne sont accessibles qu'aux membres de la communauté",
|
||||||
"community_invalidQrCode": "Code QR de communauté non valide",
|
"community_invalidQrCode": "QR code de communauté non valide",
|
||||||
"community_alreadyMember": "Déjà membre",
|
"community_alreadyMember": "Déjà membre",
|
||||||
"community_alreadyMemberMessage": "Vous êtes déjà membre de \"{name}\".",
|
"community_alreadyMemberMessage": "Vous êtes déjà membre de \"{name}\".",
|
||||||
"community_addPublicChannel": "Ajouter un Canal Public de la Communauté",
|
"community_addPublicChannel": "Ajouter un Canal Public de la Communauté",
|
||||||
"community_addPublicChannelHint": "Ajouter automatiquement le canal public pour cette communauté",
|
"community_addPublicChannelHint": "Ajouter automatiquement le canal public pour cette communauté",
|
||||||
"community_noCommunities": "Aucun groupe n'a été rejoint pour le moment.",
|
"community_noCommunities": "Aucun groupe n'a été rejoint pour le moment.",
|
||||||
"community_scanOrCreate": "Scanner un code QR ou créer une communauté pour commencer",
|
"community_scanOrCreate": "Scanner un QR code ou créer une communauté pour commencer",
|
||||||
"community_manageCommunities": "Gérer les Communautés",
|
"community_manageCommunities": "Gérer les Communautés",
|
||||||
"community_delete": "Quitter la communauté",
|
"community_delete": "Quitter la communauté",
|
||||||
"community_deleteConfirm": "Quitter \"{name}\" ?",
|
"community_deleteConfirm": "Quitter \"{name}\" ?",
|
||||||
@@ -1523,10 +1534,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"community_regenerateSecret": "Régénérer le secret",
|
"community_regenerateSecret": "Régénérer le secret",
|
||||||
"community_regenerateSecretConfirm": "Régénérer la clé secrète pour \"{name}\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.",
|
"community_regenerateSecretConfirm": "Régénérer la clé secrète pour \"{name}\" ? Tous les membres devront scanner le nouveau QR code pour continuer à communiquer.",
|
||||||
"community_regenerate": "Régénérer",
|
"community_regenerate": "Régénérer",
|
||||||
"community_secretRegenerated": "Mot de passe secret régénéré pour \"{name}\"",
|
"community_secretRegenerated": "Mot de passe secret régénéré pour \"{name}\"",
|
||||||
"community_scanToUpdateSecret": "Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"{name}\"",
|
"community_scanToUpdateSecret": "Scanner le nouveau QR code pour mettre à jour le mot de passe pour \"{name}\"",
|
||||||
"community_updateSecret": "Mettre à jour le secret",
|
"community_updateSecret": "Mettre à jour le secret",
|
||||||
"community_secretUpdated": "Modification secrète mise à jour pour \"{name}\"",
|
"community_secretUpdated": "Modification secrète mise à jour pour \"{name}\"",
|
||||||
"@contacts_pathTraceTo": {
|
"@contacts_pathTraceTo": {
|
||||||
@@ -1543,14 +1554,16 @@
|
|||||||
"contacts_pathTrace": "Traçage de chemin",
|
"contacts_pathTrace": "Traçage de chemin",
|
||||||
"contacts_repeaterPathTrace": "Tracer le chemin vers le répéteur",
|
"contacts_repeaterPathTrace": "Tracer le chemin vers le répéteur",
|
||||||
"contacts_repeaterPing": "Pinguer le répéteur",
|
"contacts_repeaterPing": "Pinguer le répéteur",
|
||||||
"contacts_roomPathTrace": "Traçage du chemin vers le serveur de la salle",
|
"contacts_roomPathTrace": "Traçage du chemin vers le room server",
|
||||||
"contacts_chatTraceRoute": "Tracer le chemin",
|
"contacts_chatTraceRoute": "Tracer le chemin",
|
||||||
"contacts_pathTraceTo": "Tracer l'itinéraire vers {name}",
|
"contacts_pathTraceTo": "Tracer l'itinéraire vers {name}",
|
||||||
"contacts_ping": "Ping",
|
"contacts_ping": "Ping",
|
||||||
"contacts_roomPing": "Pinguer le serveur de la salle",
|
"contacts_roomPing": "Pinguer le room server",
|
||||||
"contacts_invalidAdvertFormat": "Données de contact non valides",
|
"contacts_invalidAdvertFormat": "Données de contact non valides",
|
||||||
"appSettings_languageUk": "Ukrainien",
|
"appSettings_languageUk": "Ukrainien",
|
||||||
"appSettings_languageRu": "Russe",
|
"appSettings_languageRu": "Russe",
|
||||||
|
"appSettings_enableMessageTracing": "Activer le traçage des messages",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Afficher les métadonnées détaillées de routage et de synchronisation des messages",
|
||||||
"contacts_clipboardEmpty": "Le presse-papiers est vide.",
|
"contacts_clipboardEmpty": "Le presse-papiers est vide.",
|
||||||
"contacts_contactImported": "Le contact a été importé.",
|
"contacts_contactImported": "Le contact a été importé.",
|
||||||
"contacts_floodAdvert": "Annonce à tout le réseau",
|
"contacts_floodAdvert": "Annonce à tout le réseau",
|
||||||
@@ -1570,12 +1583,12 @@
|
|||||||
"notification_newNodesCount": "{count} {count, plural, =1{nouveau nœud} other{nouveaux nœuds}}",
|
"notification_newNodesCount": "{count} {count, plural, =1{nouveau nœud} other{nouveaux nœuds}}",
|
||||||
"notification_newTypeDiscovered": "Nouveau {contactType} découvert",
|
"notification_newTypeDiscovered": "Nouveau {contactType} découvert",
|
||||||
"notification_receivedNewMessage": "Nouveau message reçu",
|
"notification_receivedNewMessage": "Nouveau message reçu",
|
||||||
"settings_gpxExportRepeaters": "Exporter les répéteurs / serveur de salle au format GPX",
|
"settings_gpxExportRepeaters": "Exporter les répéteurs / room servers au format GPX",
|
||||||
"settings_gpxExportRepeatersSubtitle": "Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.",
|
"settings_gpxExportRepeatersSubtitle": "Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.",
|
||||||
"settings_gpxExportNoContacts": "Aucun contact à exporter.",
|
"settings_gpxExportNoContacts": "Aucun contact à exporter.",
|
||||||
"settings_gpxExportNotAvailable": "Non pris en charge sur votre appareil/Système d'exploitation",
|
"settings_gpxExportNotAvailable": "Non pris en charge sur votre appareil/Système d'exploitation",
|
||||||
"settings_gpxExportError": "Une erreur s'est produite lors de l'exportation.",
|
"settings_gpxExportError": "Une erreur s'est produite lors de l'exportation.",
|
||||||
"settings_gpxExportRepeatersRoom": "Emplacements des serveurs de répéteur et de salle",
|
"settings_gpxExportRepeatersRoom": "Emplacements des répéteurs et room servers",
|
||||||
"settings_gpxExportContacts": "Exporter les compagnons au format GPX",
|
"settings_gpxExportContacts": "Exporter les compagnons au format GPX",
|
||||||
"settings_gpxExportAll": "Exporter tous les contacts au format GPX",
|
"settings_gpxExportAll": "Exporter tous les contacts au format GPX",
|
||||||
"settings_gpxExportAllSubtitle": "Exporte tous les contacts avec une localisation vers un fichier GPX.",
|
"settings_gpxExportAllSubtitle": "Exporte tous les contacts avec une localisation vers un fichier GPX.",
|
||||||
@@ -1592,9 +1605,461 @@
|
|||||||
"map_removeLast": "Supprimer le dernier",
|
"map_removeLast": "Supprimer le dernier",
|
||||||
"map_runTrace": "Exécuter la traçage de chemin",
|
"map_runTrace": "Exécuter la traçage de chemin",
|
||||||
"scanner_bluetoothOffMessage": "Veuillez activer le Bluetooth pour rechercher des appareils.",
|
"scanner_bluetoothOffMessage": "Veuillez activer le Bluetooth pour rechercher des appareils.",
|
||||||
|
"scanner_chromeRequired": "Navigateur Chrome requis",
|
||||||
|
"scanner_chromeRequiredMessage": "Cette application web nécessite Google Chrome ou un navigateur basé sur Chromium pour le support Bluetooth.",
|
||||||
"scanner_bluetoothOff": "Le Bluetooth est désactivé.",
|
"scanner_bluetoothOff": "Le Bluetooth est désactivé.",
|
||||||
"scanner_enableBluetooth": "Activer le Bluetooth",
|
"scanner_enableBluetooth": "Activer le Bluetooth",
|
||||||
|
"snrIndicator_lastSeen": "Dernière fois vu",
|
||||||
|
"snrIndicator_nearByRepeaters": "Répéteurs à proximité",
|
||||||
|
"chat_ShowAllPaths": "Afficher tous les chemins",
|
||||||
"settings_clientRepeatFreqWarning": "Pour les transmissions hors réseau, il est nécessaire d'utiliser les fréquences de 433, 869 ou 918 MHz.",
|
"settings_clientRepeatFreqWarning": "Pour les transmissions hors réseau, il est nécessaire d'utiliser les fréquences de 433, 869 ou 918 MHz.",
|
||||||
"settings_clientRepeatSubtitle": "Permettez à cet appareil de répéter les paquets de données pour les autres.",
|
"settings_clientRepeatSubtitle": "Permettez à cet appareil de répéter les paquets de données pour les autres.",
|
||||||
"settings_clientRepeat": "Répétition hors réseau"
|
"settings_clientRepeat": "Répétition hors réseau",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "Données d'élévation LOS : Open-Meteo (CC BY 4.0)",
|
||||||
|
"appSettings_unitsTitle": "Unités",
|
||||||
|
"appSettings_unitsMetric": "Métrique (m/km)",
|
||||||
|
"appSettings_unitsImperial": "Impérial (ft / mi)",
|
||||||
|
"map_lineOfSight": "Ligne de vue",
|
||||||
|
"map_losScreenTitle": "Ligne de vue",
|
||||||
|
"losSelectStartEnd": "Sélectionnez les nœuds de début et de fin pour LOS.",
|
||||||
|
"losRunFailed": "Échec de la vérification de la ligne de vue : {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Effacer tous les points",
|
||||||
|
"losRunToViewElevationProfile": "Exécutez LOS pour afficher le profil d'altitude",
|
||||||
|
"losMenuTitle": "Menu LOS",
|
||||||
|
"losMenuSubtitle": "Appuyez sur les nœuds ou appuyez longuement sur la carte pour des points personnalisés",
|
||||||
|
"losShowDisplayNodes": "Afficher les nœuds d'affichage",
|
||||||
|
"losCustomPoints": "Points personnalisés",
|
||||||
|
"losCustomPointLabel": "Personnalisé {index}",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Point A",
|
||||||
|
"losPointB": "Point B",
|
||||||
|
"losAntennaA": "Antenne A : {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Antenne B : {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Exécuter la LOS",
|
||||||
|
"losNoElevationData": "Aucune donnée d'altitude",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, LOS clair, clairance minimale {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, bloqué par {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "LOS : vérification...",
|
||||||
|
"losStatusNoData": "LOS : aucune donnée",
|
||||||
|
"losStatusSummary": "LOS : {clear}/{total} clair, {blocked} bloqué, {unknown} inconnu",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Données d'altitude indisponibles pour un ou plusieurs échantillons.",
|
||||||
|
"losErrorInvalidInput": "Données de points/d'altitude non valides pour le calcul de la LOS.",
|
||||||
|
"losRenameCustomPoint": "Renommer le point personnalisé",
|
||||||
|
"losPointName": "Nom du point",
|
||||||
|
"losShowPanelTooltip": "Afficher le panneau LOS",
|
||||||
|
"losHidePanelTooltip": "Masquer le panneau LOS",
|
||||||
|
"losElevationAttribution": "Données d’altitude : Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Horizon radio",
|
||||||
|
"losLegendLosBeam": "Ligne de visée",
|
||||||
|
"losLegendTerrain": "Terrain",
|
||||||
|
"losFrequencyLabel": "Fréquence",
|
||||||
|
"losFrequencyInfoTooltip": "Voir les détails du calcul",
|
||||||
|
"losFrequencyDialogTitle": "Calcul de l’horizon radio",
|
||||||
|
"losFrequencyDialogDescription": "À partir de k={baselineK} à {baselineFreq} MHz, le calcul ajuste le facteur k pour la bande actuelle de {frequencyMHz} MHz, ce qui définit la limite incurvée de l'horizon radio.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listFilter_addToFavorites": "Ajouter à mes favoris",
|
||||||
|
"listFilter_removeFromFavorites": "Supprimer des favoris",
|
||||||
|
"listFilter_favorites": "Préférences",
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_unread": "Non lu",
|
||||||
|
"contacts_searchFavorites": "Rechercher {number}{str} Favoris...",
|
||||||
|
"contacts_searchUsers": "Rechercher {number}{str} utilisateurs...",
|
||||||
|
"contacts_searchRoomServers": "Rechercher {number}{str} room server...",
|
||||||
|
"contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...",
|
||||||
|
"contacts_searchContactsNoNumber": "Rechercher des contacts...",
|
||||||
|
"settings_contactSettings": "Paramètres de contact",
|
||||||
|
"settings_contactSettingsSubtitle": "Paramètres pour l'ajout de contacts",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Ajouter automatiquement les répéteurs",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Autoriser le compagnon à ajouter automatiquement les répéteurs découverts",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Ajouter automatiquement les room servers",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Autoriser le compagnon à ajouter automatiquement les room servers 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.",
|
||||||
|
"@radioStats_noiseFloor": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_lastRssi": {
|
||||||
|
"placeholders": {
|
||||||
|
"rssiDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_lastSnr": {
|
||||||
|
"placeholders": {
|
||||||
|
"snr": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_txAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_rxAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_stripNoise": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chat_sendCooldown": "Veuillez patienter un instant avant de réessayer.",
|
||||||
|
"appSettings_jumpToOldestUnread": "Accéder au message le plus ancien non lu",
|
||||||
|
"appSettings_languageHu": "Hongrois",
|
||||||
|
"appSettings_jumpToOldestUnreadSubtitle": "Lorsque vous ouvrez une conversation contenant des messages non lus, faites défiler la page jusqu'au premier message non lu, plutôt que jusqu'au dernier.",
|
||||||
|
"appSettings_languageJa": "Japonais",
|
||||||
|
"appSettings_languageKo": "Coréen",
|
||||||
|
"radioStats_tooltip": "Statistiques des radios et des réseaux sans fil",
|
||||||
|
"radioStats_screenTitle": "Statistiques de radio",
|
||||||
|
"radioStats_notConnected": "Connectez-vous à un appareil pour visualiser les statistiques de la radio.",
|
||||||
|
"radioStats_firmwareTooOld": "Les statistiques radio nécessitent un firmware compatible v8 ou une version ultérieure.",
|
||||||
|
"radioStats_waiting": "En attente des données…",
|
||||||
|
"radioStats_noiseFloor": "Niveau de bruit : {noiseDbm} dBm",
|
||||||
|
"radioStats_lastRssi": "Dernier RSSI : {rssiDbm} dBm",
|
||||||
|
"radioStats_lastSnr": "Dernier SNR : {snr} dB",
|
||||||
|
"radioStats_txAir": "Temps d'antenne à la télévision du Texas (total) : {seconds} s",
|
||||||
|
"radioStats_rxAir": "Temps d'utilisation de l'appareil RX (total) : {seconds} s",
|
||||||
|
"radioStats_chartCaption": "Niveau de bruit (dBm) sur les échantillons récents.",
|
||||||
|
"radioStats_stripNoise": "Niveau de bruit : {noiseDbm} dBm",
|
||||||
|
"radioStats_stripWaiting": "Récupération des statistiques de la radio…",
|
||||||
|
"radioStats_settingsTile": "Statistiques de radio",
|
||||||
|
"radioStats_settingsSubtitle": "Niveau de bruit, RSSI, rapport signal/bruit (SNR) et temps d'antenne",
|
||||||
|
"@translation_downloadFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_composerTitle": "Traduire avant d'envoyer",
|
||||||
|
"translation_enableTitle": "Activer la traduction",
|
||||||
|
"translation_title": "Traduction",
|
||||||
|
"translation_enableSubtitle": "Traduire les messages entrants et permettre la traduction avant l'envoi.",
|
||||||
|
"translation_composerSubtitle": "Contrôle l'état par défaut de l'icône de traduction du composant.",
|
||||||
|
"translation_targetLanguage": "Langue cible",
|
||||||
|
"translation_useAppLanguage": "Utiliser la langue de l'application",
|
||||||
|
"translation_downloadedModelLabel": "Modèle téléchargé",
|
||||||
|
"translation_presetModelLabel": "Modèle Hugging Face préconfiguré",
|
||||||
|
"translation_manualUrlLabel": "URL du modèle manuel",
|
||||||
|
"translation_downloadModel": "Télécharger le modèle",
|
||||||
|
"translation_downloading": "Téléchargement...",
|
||||||
|
"translation_working": "Au travail...",
|
||||||
|
"translation_stop": "Arrêtez",
|
||||||
|
"translation_mergingChunks": "Fusion des fragments téléchargés dans le fichier final...",
|
||||||
|
"translation_downloadedModels": "Modèles téléchargés",
|
||||||
|
"translation_deleteModel": "Supprimer le modèle",
|
||||||
|
"translation_modelDownloaded": "Modèle de traduction téléchargé.",
|
||||||
|
"translation_downloadStopped": "Le téléchargement a été interrompu.",
|
||||||
|
"translation_downloadFailed": "Échec du téléchargement : {error}",
|
||||||
|
"translation_enterUrlFirst": "Entrez d'abord l'URL du modèle.",
|
||||||
|
"@scanner_linuxPairingPinPrompt": {
|
||||||
|
"placeholders": {
|
||||||
|
"deviceName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@translation_translateTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"language": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_composerEnabledHint": "Les messages seront traduits avant d'être envoyés.",
|
||||||
|
"translation_translateBeforeSending": "Traduire avant d'envoyer",
|
||||||
|
"translation_composerDisabledHint": "Envoyez des messages dans la langue originale, telle que vous l'avez tapée.",
|
||||||
|
"translation_messageTranslation": "Traduction du message",
|
||||||
|
"translation_translateTo": "Traduire en {language}",
|
||||||
|
"translation_translationOptions": "Options de traduction",
|
||||||
|
"translation_systemLanguage": "Langue du système",
|
||||||
|
"scanner_linuxPairingPinTitle": "Code PIN pour la connexion Bluetooth",
|
||||||
|
"scanner_linuxPairingHidePin": "Masquer le code PIN",
|
||||||
|
"scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si nécessaire).",
|
||||||
|
"scanner_linuxPairingShowPin": "Afficher le code PIN",
|
||||||
|
"repeater_cliQuickClockSync": "Synchronisation de l'horloge",
|
||||||
|
"repeater_cliQuickDiscovery": "Découvrir les voisins"
|
||||||
}
|
}
|
||||||
+2103
File diff suppressed because it is too large
Load Diff
+469
-4
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"channels_channelDeleteFailed": "Impossibile eliminare il canale \"{name}\"",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@@locale": "it",
|
"@@locale": "it",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Contatti",
|
"nav_contacts": "Contatti",
|
||||||
@@ -277,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": {
|
||||||
@@ -334,6 +343,8 @@
|
|||||||
"channels_publicChannel": "Canale pubblico",
|
"channels_publicChannel": "Canale pubblico",
|
||||||
"channels_privateChannel": "Canale privato",
|
"channels_privateChannel": "Canale privato",
|
||||||
"channels_editChannel": "Modifica canale",
|
"channels_editChannel": "Modifica canale",
|
||||||
|
"channels_muteChannel": "Silenzia canale",
|
||||||
|
"channels_unmuteChannel": "Attiva notifiche canale",
|
||||||
"channels_deleteChannel": "Elimina canale",
|
"channels_deleteChannel": "Elimina canale",
|
||||||
"channels_deleteChannelConfirm": "Eliminare \"{name}\"? Non può essere annullato.",
|
"channels_deleteChannelConfirm": "Eliminare \"{name}\"? Non può essere annullato.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
@@ -1351,12 +1362,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Vicini",
|
"repeater_neighbors": "Vicini",
|
||||||
"repeater_neighboursSubtitle": "Visualizza vicini di salto pari a zero.",
|
"repeater_neighborsSubtitle": "Visualizza vicini di salto pari a zero.",
|
||||||
"neighbors_receivedData": "Ricevute dati vicini",
|
"neighbors_receivedData": "Ricevute dati vicini",
|
||||||
"neighbors_requestTimedOut": "I vicini richiedono un timeout.",
|
"neighbors_requestTimedOut": "I vicini richiedono un timeout.",
|
||||||
"neighbors_errorLoading": "Errore nel caricamento dei vicini: {error}",
|
"neighbors_errorLoading": "Errore nel caricamento dei vicini: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Ripetitori Vicini",
|
"neighbors_repeatersNeighbors": "Ripetitori Vicini",
|
||||||
"neighbors_noData": "Nessun dato sugli vicini disponibile.",
|
"neighbors_noData": "Nessun dato sugli vicini disponibile.",
|
||||||
"channels_createPrivateChannel": "Crea un Canale Privato",
|
"channels_createPrivateChannel": "Crea un Canale Privato",
|
||||||
"channels_createPrivateChannelDesc": "Protetta con una chiave segreta.",
|
"channels_createPrivateChannelDesc": "Protetta con una chiave segreta.",
|
||||||
@@ -1551,6 +1562,8 @@
|
|||||||
"appSettings_languageRu": "Russo",
|
"appSettings_languageRu": "Russo",
|
||||||
"contacts_invalidAdvertFormat": "Dati di contatto non validi",
|
"contacts_invalidAdvertFormat": "Dati di contatto non validi",
|
||||||
"appSettings_languageUk": "Ucraino",
|
"appSettings_languageUk": "Ucraino",
|
||||||
|
"appSettings_enableMessageTracing": "Abilita tracciamento messaggi",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Mostra metadati dettagliati su instradamento e tempi per i messaggi",
|
||||||
"contacts_zeroHopAdvert": "Annuncio Zero Hop",
|
"contacts_zeroHopAdvert": "Annuncio Zero Hop",
|
||||||
"contacts_floodAdvert": "Annuncio alluvionale",
|
"contacts_floodAdvert": "Annuncio alluvionale",
|
||||||
"contacts_copyAdvertToClipboard": "Copia Annuncio negli Appunti",
|
"contacts_copyAdvertToClipboard": "Copia Annuncio negli Appunti",
|
||||||
@@ -1593,8 +1606,460 @@
|
|||||||
"map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.",
|
"map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.",
|
||||||
"scanner_bluetoothOff": "Il Bluetooth è disattivato.",
|
"scanner_bluetoothOff": "Il Bluetooth è disattivato.",
|
||||||
"scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.",
|
"scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.",
|
||||||
|
"scanner_chromeRequired": "Browser Chrome richiesto",
|
||||||
|
"scanner_chromeRequiredMessage": "Questa applicazione web richiede Google Chrome o un browser basato su Chromium per il supporto Bluetooth.",
|
||||||
"scanner_enableBluetooth": "Abilita il Bluetooth",
|
"scanner_enableBluetooth": "Abilita il Bluetooth",
|
||||||
|
"snrIndicator_nearByRepeaters": "Ripetitori vicini",
|
||||||
|
"snrIndicator_lastSeen": "Ultimo accesso",
|
||||||
|
"chat_ShowAllPaths": "Mostra tutti i percorsi",
|
||||||
"settings_clientRepeat": "Ripetizione \"fuori dalla rete\"",
|
"settings_clientRepeat": "Ripetizione \"fuori dalla rete\"",
|
||||||
"settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.",
|
"settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.",
|
||||||
"settings_clientRepeatSubtitle": "Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri."
|
"settings_clientRepeatSubtitle": "Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri.",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "Dati di elevazione LOS: Open-Meteo (CC BY 4.0)",
|
||||||
|
"appSettings_unitsTitle": "Unità",
|
||||||
|
"appSettings_unitsMetric": "Metrico (m/km)",
|
||||||
|
"appSettings_unitsImperial": "Imperiale (ft / mi)",
|
||||||
|
"map_lineOfSight": "Linea di vista",
|
||||||
|
"map_losScreenTitle": "Linea di vista",
|
||||||
|
"losSelectStartEnd": "Seleziona i nodi iniziali e finali per la LOS.",
|
||||||
|
"losRunFailed": "Controllo della linea di vista fallito: {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Cancella tutti i punti",
|
||||||
|
"losRunToViewElevationProfile": "Eseguire LOS per visualizzare il profilo altimetrico",
|
||||||
|
"losMenuTitle": "Menù LOS",
|
||||||
|
"losMenuSubtitle": "Tocca i nodi o premi a lungo la mappa per punti personalizzati",
|
||||||
|
"losShowDisplayNodes": "Mostra i nodi di visualizzazione",
|
||||||
|
"losCustomPoints": "Punti personalizzati",
|
||||||
|
"losCustomPointLabel": "Personalizzato {index}",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Punto A",
|
||||||
|
"losPointB": "Punto B",
|
||||||
|
"losAntennaA": "Antenna A: {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Antenna B: {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Esegui LOS",
|
||||||
|
"losNoElevationData": "Nessun dato di elevazione",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, libera LOS, distanza minima {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, bloccato da {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "LOS: controllo...",
|
||||||
|
"losStatusNoData": "LOS: nessun dato",
|
||||||
|
"losStatusSummary": "LOS: {clear}/{total} libera, {blocked} bloccato, {unknown} sconosciuto",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Dati di elevazione non disponibili per uno o più campioni.",
|
||||||
|
"losErrorInvalidInput": "Dati punti/elevazione non validi per il calcolo della LOS.",
|
||||||
|
"losRenameCustomPoint": "Rinomina punto personalizzato",
|
||||||
|
"losPointName": "Nome del punto",
|
||||||
|
"losShowPanelTooltip": "Mostra il pannello LOS",
|
||||||
|
"losHidePanelTooltip": "Nascondi il pannello LOS",
|
||||||
|
"losElevationAttribution": "Dati di elevazione: Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Orizzonte radio",
|
||||||
|
"losLegendLosBeam": "Linea di vista",
|
||||||
|
"losLegendTerrain": "Terreno",
|
||||||
|
"losFrequencyLabel": "Frequenza",
|
||||||
|
"losFrequencyInfoTooltip": "Visualizza i dettagli del calcolo",
|
||||||
|
"losFrequencyDialogTitle": "Calcolo dell’orizzonte radio",
|
||||||
|
"losFrequencyDialogDescription": "Partendo da k={baselineK} a {baselineFreq} MHz, il calcolo regola il fattore k per l'attuale banda {frequencyMHz} MHz, che definisce il limite curvo dell'orizzonte radio.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listFilter_addToFavorites": "Aggiungi ai preferiti",
|
||||||
|
"listFilter_removeFromFavorites": "Rimuovi dai preferiti",
|
||||||
|
"listFilter_favorites": "Preferiti",
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_searchUsers": "Cerca {number}{str} Utenti...",
|
||||||
|
"contacts_searchContactsNoNumber": "Cerca Contatti...",
|
||||||
|
"contacts_searchFavorites": "Cerca {number}{str} Preferiti...",
|
||||||
|
"contacts_unread": "Non letti",
|
||||||
|
"contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...",
|
||||||
|
"contacts_searchRoomServers": "Cerca {number}{str} server Room...",
|
||||||
|
"contactsSettings_title": "Impostazioni dei contatti",
|
||||||
|
"settings_contactSettings": "Impostazioni di contatto",
|
||||||
|
"contactsSettings_otherTitle": "Altre impostazioni relative ai contatti",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Consenti al compagno di aggiungere automaticamente gli utenti scoperti.",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Aggiungere ripetitori automaticamente",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Consenti al compagno di aggiungere automaticamente i server delle stanze scoperte.",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Aggiungere automaticamente i sensori",
|
||||||
|
"settings_contactSettingsSubtitle": "Impostazioni per l'aggiunta dei contatti",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Aggiungere utenti automaticamente",
|
||||||
|
"contactsSettings_autoAddTitle": "Scoperta automatica",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Consenti al compagno di aggiungere automaticamente i sensori scoperti",
|
||||||
|
"discoveredContacts_noMatching": "Nessun contatto corrispondente",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Consenti al compagno di aggiungere automaticamente i ripetitori scoperti.",
|
||||||
|
"discoveredContacts_searchHint": "Cerca contatti scoperti",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Aggiungere automaticamente i server delle stanze",
|
||||||
|
"discoveredContacts_addContact": "Aggiungi contatto",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Sostituisci il più vecchio",
|
||||||
|
"discoveredContacts_Title": "Contatti scoperti",
|
||||||
|
"discoveredContacts_contactAdded": "Contatto aggiunto",
|
||||||
|
"discoveredContacts_deleteContact": "Elimina Contatto",
|
||||||
|
"discoveredContacts_copyContact": "Copia contatto negli appunti",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Quando l'elenco dei contatti è pieno, il contatto più vecchio non tra i preferiti verrà sostituito.",
|
||||||
|
"common_deleteAll": "Elimina tutto",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Sei sicuro di voler eliminare tutti i contatti scoperti?",
|
||||||
|
"discoveredContacts_deleteContactAll": "Eliminare tutti i contatti scoperti",
|
||||||
|
"map_guessedLocation": "Località indovinata",
|
||||||
|
"map_showGuessedLocations": "Mostra le posizioni stimate dei nodi",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenSubtitle": "Seleziona il dispositivo seriale rilevato e connettilo direttamente al tuo nodo MeshCore.",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenTitle": "Connessione tramite USB",
|
||||||
|
"usbScreenStatus": "Seleziona un dispositivo USB",
|
||||||
|
"usbScreenNote": "La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.",
|
||||||
|
"usbScreenEmptyState": "Nessun dispositivo USB rilevato. Collegare uno e aggiornare.",
|
||||||
|
"usbErrorPermissionDenied": "È stato negato l'accesso tramite USB.",
|
||||||
|
"usbErrorDeviceMissing": "Il dispositivo USB selezionato non è più disponibile.",
|
||||||
|
"usbErrorInvalidPort": "Seleziona un dispositivo USB valido.",
|
||||||
|
"usbErrorBusy": "Un'altra richiesta di connessione tramite USB è già in corso.",
|
||||||
|
"usbErrorNotConnected": "Non è collegato alcun dispositivo USB.",
|
||||||
|
"usbErrorOpenFailed": "Impossibile aprire il dispositivo USB selezionato.",
|
||||||
|
"usbErrorConnectFailed": "Impossibile connettersi al dispositivo USB selezionato.",
|
||||||
|
"usbErrorUnsupported": "La comunicazione seriale tramite USB non è supportata su questa piattaforma.",
|
||||||
|
"usbErrorAlreadyActive": "La connessione USB è già attiva.",
|
||||||
|
"usbErrorNoDeviceSelected": "Non è stato selezionato alcun dispositivo USB.",
|
||||||
|
"usbErrorPortClosed": "La connessione USB non è attiva.",
|
||||||
|
"usbFallbackDeviceName": "Dispositivo per comunicazione seriale su rete",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_searching": "Ricerca di dispositivi USB...",
|
||||||
|
"usbConnectionFailed": "Errore nella connessione USB: {error}",
|
||||||
|
"usbStatus_notConnected": "Seleziona un dispositivo USB",
|
||||||
|
"usbStatus_connecting": "Connessione al dispositivo USB...",
|
||||||
|
"usbErrorConnectTimedOut": "La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion.",
|
||||||
|
"@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",
|
||||||
|
"@radioStats_noiseFloor": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_lastRssi": {
|
||||||
|
"placeholders": {
|
||||||
|
"rssiDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_lastSnr": {
|
||||||
|
"placeholders": {
|
||||||
|
"snr": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_txAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_rxAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_stripNoise": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"appSettings_jumpToOldestUnreadSubtitle": "Quando si apre una chat con messaggi non letti, scorrete verso l'alto fino al primo messaggio non letto, invece che al più recente.",
|
||||||
|
"chat_sendCooldown": "Si prega di attendere un momento prima di inviare nuovamente.",
|
||||||
|
"appSettings_jumpToOldestUnread": "Vai al messaggio più vecchio non letto",
|
||||||
|
"appSettings_languageHu": "Ungherese",
|
||||||
|
"appSettings_languageJa": "Giapponese",
|
||||||
|
"appSettings_languageKo": "Coreano",
|
||||||
|
"radioStats_tooltip": "Statistiche per radio e reti",
|
||||||
|
"radioStats_screenTitle": "Statistiche radio",
|
||||||
|
"radioStats_notConnected": "Connettiti a un dispositivo per visualizzare le statistiche radio.",
|
||||||
|
"radioStats_firmwareTooOld": "Le statistiche radio richiedono il firmware versione 8 o successiva.",
|
||||||
|
"radioStats_noiseFloor": "Livello di rumore: {noiseDbm} dBm",
|
||||||
|
"radioStats_waiting": "In attesa dei dati…",
|
||||||
|
"radioStats_lastRssi": "Ultimo valore RSSI: {rssiDbm} dBm",
|
||||||
|
"radioStats_lastSnr": "Ultimo SNR: {snr} dB",
|
||||||
|
"radioStats_txAir": "Tempo di trasmissione in diretta (totale): {seconds} s",
|
||||||
|
"radioStats_rxAir": "Tempo di trasmissione RX (totale): {seconds} s",
|
||||||
|
"radioStats_chartCaption": "Livello di rumore (dBm) misurato su campioni recenti.",
|
||||||
|
"radioStats_stripNoise": "Livello di rumore: {noiseDbm} dBm",
|
||||||
|
"radioStats_stripWaiting": "Recupero delle statistiche radio…",
|
||||||
|
"radioStats_settingsTile": "Statistiche radio",
|
||||||
|
"radioStats_settingsSubtitle": "Livello di rumore, RSSI, rapporto segnale/rumore (SNR) e tempo di trasmissione",
|
||||||
|
"@translation_downloadFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_composerTitle": "Tradurre prima di inviare",
|
||||||
|
"translation_enableSubtitle": "Tradurre i messaggi in arrivo e consentire la traduzione preventiva prima dell'invio.",
|
||||||
|
"translation_enableTitle": "Abilitare la traduzione",
|
||||||
|
"translation_title": "Traduzione",
|
||||||
|
"translation_composerSubtitle": "Controlla lo stato predefinito dell'icona di traduzione del compositore.",
|
||||||
|
"translation_targetLanguage": "Lingua di destinazione",
|
||||||
|
"translation_useAppLanguage": "Utilizza la lingua dell'app",
|
||||||
|
"translation_downloadedModelLabel": "Modello scaricato",
|
||||||
|
"translation_presetModelLabel": "Modello predefinito di Hugging Face",
|
||||||
|
"translation_manualUrlLabel": "URL del modello manuale",
|
||||||
|
"translation_downloadModel": "Scarica il modello",
|
||||||
|
"translation_downloading": "Inizio download...",
|
||||||
|
"translation_working": "Lavoro...",
|
||||||
|
"translation_stop": "Smetta",
|
||||||
|
"translation_downloadedModels": "Modelli scaricati",
|
||||||
|
"translation_mergingChunks": "Unione dei frammenti scaricati in un unico file...",
|
||||||
|
"translation_deleteModel": "Elimina modello",
|
||||||
|
"translation_modelDownloaded": "Modello di traduzione scaricato.",
|
||||||
|
"translation_downloadStopped": "Il download è stato interrotto.",
|
||||||
|
"translation_downloadFailed": "Download fallito: {error}",
|
||||||
|
"translation_enterUrlFirst": "Inserite innanzitutto l'URL del modello.",
|
||||||
|
"@scanner_linuxPairingPinPrompt": {
|
||||||
|
"placeholders": {
|
||||||
|
"deviceName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@translation_translateTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"language": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_messageTranslation": "Traduzione del messaggio",
|
||||||
|
"translation_translateBeforeSending": "Tradurre prima di inviare",
|
||||||
|
"translation_composerDisabledHint": "Invia messaggi utilizzando la lingua originale, scritta.",
|
||||||
|
"translation_composerEnabledHint": "I messaggi verranno tradotti prima di essere inviati.",
|
||||||
|
"translation_translateTo": "Tradurre in {language}",
|
||||||
|
"translation_translationOptions": "Opzioni di traduzione",
|
||||||
|
"translation_systemLanguage": "Lingua del sistema",
|
||||||
|
"scanner_linuxPairingPinPrompt": "Inserire il codice PIN per {deviceName} (lasciare vuoto se non presente).",
|
||||||
|
"scanner_linuxPairingShowPin": "Mostra PIN",
|
||||||
|
"scanner_linuxPairingPinTitle": "PIN per l'accoppiamento Bluetooth",
|
||||||
|
"scanner_linuxPairingHidePin": "Nascondi il PIN",
|
||||||
|
"repeater_cliQuickClockSync": "Sincronizzazione dell'orologio",
|
||||||
|
"repeater_cliQuickDiscovery": "Scopri i Vicini"
|
||||||
}
|
}
|
||||||
+2103
File diff suppressed because it is too large
Load Diff
+2103
File diff suppressed because it is too large
Load Diff
+1369
-25
File diff suppressed because it is too large
Load Diff
@@ -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 => 'Действия';
|
||||||
|
|
||||||
@@ -326,6 +517,10 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
String get settings_aboutDescription =>
|
String get settings_aboutDescription =>
|
||||||
'Отворен софтуер за Flutter клиент за MeshCore LoRa мрежови устройства.';
|
'Отворен софтуер за Flutter клиент за MeshCore LoRa мрежови устройства.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_aboutOpenMeteoAttribution =>
|
||||||
|
'Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_infoName => 'Име';
|
String get settings_infoName => 'Име';
|
||||||
|
|
||||||
@@ -462,6 +657,14 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageUk => 'Украински';
|
String get appSettings_languageUk => 'Украински';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracing =>
|
||||||
|
'Разрешаване на проследяване на съобщения';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracingSubtitle =>
|
||||||
|
'Показване на подробни метаданни за маршрутизация и синхронизация за съобщения';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Уведомления';
|
String get appSettings_notifications => 'Уведомления';
|
||||||
|
|
||||||
@@ -538,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 => 'Батерия';
|
||||||
|
|
||||||
@@ -622,6 +870,15 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_offlineMapCache => 'Кеш на офлайн карти';
|
String get appSettings_offlineMapCache => 'Кеш на офлайн карти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsTitle => 'единици';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsMetric => 'Метрика (m / km)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsImperial => 'Имперска (ft / mi)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_noAreaSelected => 'Няма избрана област';
|
String get appSettings_noAreaSelected => 'Няма избрана област';
|
||||||
|
|
||||||
@@ -660,7 +917,35 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
'Контактите ще се появят, когато устройствата рекламират.';
|
'Контактите ще се появят, когато устройствата рекламират.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_searchContacts => 'Търсене на контакти...';
|
String get contacts_unread => 'Непрочетено';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_searchContactsNoNumber => 'Търси контакти...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchContacts(int number, String str) {
|
||||||
|
return 'Търсене на контакти...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchFavorites(int number, String str) {
|
||||||
|
return 'Търсене на $number$str любими...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchUsers(int number, String str) {
|
||||||
|
return 'Търсене на $number$str потребители...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRepeaters(int number, String str) {
|
||||||
|
return 'Търсене на $number$str повтарящи се...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRoomServers(int number, String str) {
|
||||||
|
return 'Търсене на $number$str сървъри в стаята...';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_noUnreadContacts => 'Няма непрочетени контакти';
|
String get contacts_noUnreadContacts => 'Няма непрочетени контакти';
|
||||||
@@ -708,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\" вече съществува.';
|
||||||
@@ -747,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 => 'Канали';
|
||||||
|
|
||||||
@@ -785,6 +1109,12 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_editChannel => 'Редактирай канал';
|
String get channels_editChannel => 'Редактирай канал';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_muteChannel => 'Заглуши канала';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_unmuteChannel => 'Включи известията на канала';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_deleteChannel => 'Изтрий канала';
|
String get channels_deleteChannel => 'Изтрий канала';
|
||||||
|
|
||||||
@@ -793,6 +1123,11 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
return 'Изтрий \"$name\"? Това не може да бъде отменено.';
|
return 'Изтрий \"$name\"? Това не може да бъде отменено.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channels_channelDeleteFailed(String name) {
|
||||||
|
return 'Неуспешно изтриване на канала \"$name\"';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String channels_channelDeleted(String name) {
|
String channels_channelDeleted(String name) {
|
||||||
return 'Каналът \"$name\" е изтрит';
|
return 'Каналът \"$name\" е изтрит';
|
||||||
@@ -904,6 +1239,14 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_noMessages => 'Няма съобщения.';
|
String get chat_noMessages => 'Няма съобщения.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_sendMessage => 'Send message';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String chat_sendMessageTo(String contactName) {
|
||||||
|
return 'Изпрати съобщение на $contactName';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_sendMessageToStart => 'Изпрати съобщение, за да започнеш.';
|
String get chat_sendMessageToStart => 'Изпрати съобщение, за да започнеш.';
|
||||||
|
|
||||||
@@ -923,11 +1266,6 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_location => 'Местоположение';
|
String get chat_location => 'Местоположение';
|
||||||
|
|
||||||
@override
|
|
||||||
String chat_sendMessageTo(String contactName) {
|
|
||||||
return 'Изпрати съобщение на $contactName';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_typeMessage => 'Въведете съобщение...';
|
String get chat_typeMessage => 'Въведете съобщение...';
|
||||||
|
|
||||||
@@ -1080,6 +1418,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_pathManagement => 'Управление на пътища';
|
String get chat_pathManagement => 'Управление на пътища';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_ShowAllPaths => 'Покажи всички пътища';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_routingMode => 'Режим на маршрутизиране';
|
String get chat_routingMode => 'Режим на маршрутизиране';
|
||||||
|
|
||||||
@@ -1240,6 +1581,12 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_title => 'Карта на възлите';
|
String get map_title => 'Карта на възлите';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_lineOfSight => 'Линия на видимост';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_losScreenTitle => 'Линия на видимост';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_noNodesWithLocation => 'Няма възли с данни за местоположение.';
|
String get map_noNodesWithLocation => 'Няма възли с данни за местоположение.';
|
||||||
|
|
||||||
@@ -1297,6 +1644,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 => 'Етикетиране на пин';
|
||||||
|
|
||||||
@@ -1342,6 +1692,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 => 'Префикс на ключа';
|
||||||
|
|
||||||
@@ -1357,6 +1710,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 => 'Последна видяна дата';
|
||||||
|
|
||||||
@@ -1376,6 +1739,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 => 'Премахни Последно';
|
||||||
|
|
||||||
@@ -1677,10 +2043,10 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Изпрати команди към ретранслатора';
|
String get repeater_cliSubtitle => 'Изпрати команди към ретранслатора';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Съседи';
|
String get repeater_neighbors => 'Съседи';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle =>
|
String get repeater_neighborsSubtitle =>
|
||||||
'Преглед на съседни възли с нулев скок.';
|
'Преглед на съседни възли с нулев скок.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -2066,6 +2432,12 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get repeater_cliQuickClock => 'Часовник';
|
String get repeater_cliQuickClock => 'Часовник';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get repeater_cliQuickClockSync => 'Синхронизация на часовника';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get repeater_cliQuickDiscovery => 'Открий Съседи';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_cliHelpAdvert => 'Изпраща рекламен пакет';
|
String get repeater_cliHelpAdvert => 'Изпраща рекламен пакет';
|
||||||
|
|
||||||
@@ -2380,7 +2752,7 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Повторители Съседи';
|
String get neighbors_repeatersNeighbors => 'Повторители Съседи';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Няма налични данни за съседи.';
|
String get neighbors_noData => 'Няма налични данни за съседи.';
|
||||||
@@ -2687,6 +3059,15 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get listFilter_all => 'Всички';
|
String get listFilter_all => 'Всички';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_favorites => 'Любими';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_addToFavorites => 'Добави към любими';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_removeFromFavorites => 'Премахване от списъка с любими';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_users => 'Потребители';
|
String get listFilter_users => 'Потребители';
|
||||||
|
|
||||||
@@ -2721,6 +3102,144 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get pathTrace_clearTooltip => 'Изчисти пътя';
|
String get pathTrace_clearTooltip => 'Изчисти пътя';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectStartEnd => 'Изберете начални и крайни възли за LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losRunFailed(String error) {
|
||||||
|
return 'Проверката на пряката видимост е неуспешна: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losClearAllPoints => 'Изчистете всички точки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRunToViewElevationProfile =>
|
||||||
|
'Стартирайте LOS, за да видите профила на надморската височина';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuTitle => 'LOS меню';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuSubtitle =>
|
||||||
|
'Докоснете възли или натиснете продължително карта за персонализирани точки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowDisplayNodes => 'Показване на възли на дисплея';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losCustomPoints => 'Персонализирани точки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losCustomPointLabel(int index) {
|
||||||
|
return 'Персонализирано $index';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointA => 'Точка А';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointB => 'Точка Б';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaA(String value, String unit) {
|
||||||
|
return 'Антена A: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaB(String value, String unit) {
|
||||||
|
return 'Антена B: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRun => 'Стартирайте LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losNoElevationData => 'Няма данни за надморска височина';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileClear(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String clearance,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, чист LOS, минимално разстояние $clearance $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileBlocked(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, блокиран от $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusChecking => 'LOS: проверка...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusNoData => 'LOS: няма данни';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||||
|
return 'LOS: $clear/$total ясно, $blocked блокирано, $unknown неизвестно';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorElevationUnavailable =>
|
||||||
|
'Няма налични данни за надморска височина за една или повече проби.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorInvalidInput =>
|
||||||
|
'Невалидни данни за точки/надморска височина за изчисляване на LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRenameCustomPoint => 'Преименувайте персонализирана точка';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointName => 'Име на точката';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowPanelTooltip => 'Показване на LOS панел';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losHidePanelTooltip => 'Скриване на LOS панела';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losElevationAttribution =>
|
||||||
|
'Данни за надморска височина: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendRadioHorizon => 'Радиохоризонт';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendLosBeam => 'Линия на видимост';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendTerrain => 'Терен';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyLabel => 'Честота';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyInfoTooltip => 'Преглед на детайли за изчислението';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyDialogTitle => 'Изчисляване на радиохоризонта';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losFrequencyDialogDescription(
|
||||||
|
double baselineK,
|
||||||
|
double baselineFreq,
|
||||||
|
double frequencyMHz,
|
||||||
|
double kFactor,
|
||||||
|
) {
|
||||||
|
return 'Започвайки от k=$baselineK при $baselineFreq MHz, изчислението коригира k-фактора за текущата $frequencyMHz MHz лента, която определя границата на извития радиохоризонт.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_pathTrace => 'Пътен проследяване';
|
String get contacts_pathTrace => 'Пътен проследяване';
|
||||||
|
|
||||||
@@ -2890,4 +3409,280 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open износ на данни за карта в формат GPX';
|
'meshcore-open износ на данни за карта в формат GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Близки повтарящи се устройства';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Последно видян';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_title => 'Настройки на контактите';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddTitle => 'Автоматично откриване';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_otherTitle =>
|
||||||
|
'Други настройки свързани с контакти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersTitle =>
|
||||||
|
'Автоматично добавяне на потребители';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersSubtitle =>
|
||||||
|
'Позволи на спътника да добавя автоматично откритите потребители.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersTitle =>
|
||||||
|
'Автоматично добавяне на повтарящи се елементи';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||||
|
'Позволи на спътника да добавя автоматично откритите повтарящи се устройства.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersTitle =>
|
||||||
|
'Автоматично добавяне на сървъри на стаите';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||||
|
'Позволи на спътника да добавя автоматично откритите сървъри на стаите.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsTitle =>
|
||||||
|
'Автоматично добавяне на датчици';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||||
|
'Позволи на спътника да добавя автоматично откритите датчици.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestTitle => 'Премахни най-старото';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestSubtitle =>
|
||||||
|
'Когато списъкът с контакти е пълен, най-старият неключов контакт ще бъде заменен.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_Title => 'Открити контакти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_noMatching => 'Няма съвпадащи контакти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_searchHint => 'Търсене на открити контакти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_contactAdded => 'Контакт добавен';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_addContact => 'Добави контакт';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_copyContact => 'Копирай контакт в клипборда';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContact => 'Изтрий контакт';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAll =>
|
||||||
|
'Изтриване на Всички Открити Контакти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAllContent =>
|
||||||
|
'Сигурни ли сте, че искате да изтриете всички открити контакти?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_sendCooldown =>
|
||||||
|
'Моля, изчакайте малко, преди да изпратите отново.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_jumpToOldestUnread =>
|
||||||
|
'Преминете към най-старата непочетена статия';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_jumpToOldestUnreadSubtitle =>
|
||||||
|
'Когато отворите чат с непрочетени съобщения, плъзнете надолу, за да видите първото непрочетено съобщение, вместо най-новото.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageHu => 'Унгарски';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageJa => 'Японски';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageKo => 'Корейски';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_tooltip => 'Статистика за радио и мрежа';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_screenTitle =>
|
||||||
|
'Статистически данни за радиопредаванията';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_notConnected =>
|
||||||
|
'Свържете се с устройство, за да видите статистически данни за радиопредаване.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_firmwareTooOld =>
|
||||||
|
'Статистиката на радиостанцията изисква съвместимо софтуерно решение версия 8 или по-нова.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_waiting => 'Изчакване на данни…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_noiseFloor(int noiseDbm) {
|
||||||
|
return 'Ниво на шума: $noiseDbm dBm';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_lastRssi(int rssiDbm) {
|
||||||
|
return 'Последен RSSI: $rssiDbm dBm';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_lastSnr(String snr) {
|
||||||
|
return 'Последна стойност на SNR: $snr dB';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_txAir(int seconds) {
|
||||||
|
return 'Време на въздух (общо): $seconds секунди';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_rxAir(int seconds) {
|
||||||
|
return 'Общо време на използване на RX (в секунди): $seconds с';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_chartCaption =>
|
||||||
|
'Ниво на шума (dBm) за последните измервания.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_stripNoise(int noiseDbm) {
|
||||||
|
return 'Ниво на шума: $noiseDbm dBm';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_stripWaiting => 'Извличане на данни за радиото…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_settingsTile => 'Статистически данни за радиостанции';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_settingsSubtitle =>
|
||||||
|
'Ниво на шума, RSSI, SNR и време на пренос';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_title => 'Превод';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_enableTitle => 'Активирайте превода';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_enableSubtitle =>
|
||||||
|
'Превеждайте входящите съобщения и позволявайте предварително превеждане преди изпращане.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerTitle => 'Преведете преди да изпратите';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerSubtitle =>
|
||||||
|
'Контролира началния статус на иконата за превод, създадена от композитора.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_targetLanguage => 'Целеви език';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_useAppLanguage => 'Използвайте езика на приложението';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadedModelLabel => 'Изтегнат модел';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_presetModelLabel =>
|
||||||
|
'Предварително конфигуриран модел от Hugging Face';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_manualUrlLabel => 'URL на ръководството';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadModel => 'Изтеглете модела';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloading => 'Изтегляне...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_working => 'Работа...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_stop => 'Спрете';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_mergingChunks =>
|
||||||
|
'Съединяване на изтеглените части в един файл...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadedModels => 'Изтеглени модели';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_deleteModel => 'Изтриване на модела';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_modelDownloaded => 'Моделът за превод е изтеглен.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadStopped => 'Изтеглянето беше прекъснато.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String translation_downloadFailed(String error) {
|
||||||
|
return 'Не успях да изтегля: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_enterUrlFirst => 'Въведете първо URL адрес на модела.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_linuxPairingShowPin => 'Покажи PIN';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_linuxPairingHidePin => 'Скриване на PIN кода';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_linuxPairingPinTitle => 'PIN за съвпадение чрез Bluetooth';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||||
|
return 'Въведете PIN кода за $deviceName (оставете празно, ако няма такъв).';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_messageTranslation => 'Превод на съобщението';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_translateBeforeSending =>
|
||||||
|
'Преведете преди да изпратите';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerEnabledHint =>
|
||||||
|
'Съобщенията ще бъдат преведени, преди да бъдат изпратени.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerDisabledHint =>
|
||||||
|
'Изпращайте съобщения на оригиналния въведен език.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String translation_translateTo(String language) {
|
||||||
|
return 'Превеждане на $language';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_translationOptions => 'Опции за превод';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_systemLanguage => 'Език на системата';
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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';
|
||||||
|
|
||||||
@@ -322,6 +516,10 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
String get settings_aboutDescription =>
|
String get settings_aboutDescription =>
|
||||||
'Un client Flutter open-source per i dispositivi di rete mesh LoRa Core di MeshCore.';
|
'Un client Flutter open-source per i dispositivi di rete mesh LoRa Core di MeshCore.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_aboutOpenMeteoAttribution =>
|
||||||
|
'Dati di elevazione LOS: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_infoName => 'Nome';
|
String get settings_infoName => 'Nome';
|
||||||
|
|
||||||
@@ -458,6 +656,14 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageUk => 'Ucraino';
|
String get appSettings_languageUk => 'Ucraino';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracing =>
|
||||||
|
'Abilita tracciamento messaggi';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracingSubtitle =>
|
||||||
|
'Mostra metadati dettagliati su instradamento e tempi per i messaggi';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Notifiche';
|
String get appSettings_notifications => 'Notifiche';
|
||||||
|
|
||||||
@@ -535,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';
|
||||||
|
|
||||||
@@ -619,6 +869,15 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_offlineMapCache => 'Cache Mappa Offline';
|
String get appSettings_offlineMapCache => 'Cache Mappa Offline';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsTitle => 'Unità';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsMetric => 'Metrico (m/km)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsImperial => 'Imperiale (ft / mi)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_noAreaSelected => 'Nessun\'area selezionata';
|
String get appSettings_noAreaSelected => 'Nessun\'area selezionata';
|
||||||
|
|
||||||
@@ -656,7 +915,35 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
'I contatti appariranno quando i dispositivi pubblicizzano.';
|
'I contatti appariranno quando i dispositivi pubblicizzano.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_searchContacts => 'Cerca contatti...';
|
String get contacts_unread => 'Non letti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_searchContactsNoNumber => 'Cerca Contatti...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchContacts(int number, String str) {
|
||||||
|
return 'Cerca contatti...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchFavorites(int number, String str) {
|
||||||
|
return 'Cerca $number$str Preferiti...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchUsers(int number, String str) {
|
||||||
|
return 'Cerca $number$str Utenti...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRepeaters(int number, String str) {
|
||||||
|
return 'Cerca $number$str Ripetitori...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRoomServers(int number, String str) {
|
||||||
|
return 'Cerca $number$str server Room...';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_noUnreadContacts => 'Nessun contatto non letto';
|
String get contacts_noUnreadContacts => 'Nessun contatto non letto';
|
||||||
@@ -704,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à.';
|
||||||
@@ -743,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';
|
||||||
|
|
||||||
@@ -781,6 +1107,12 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_editChannel => 'Modifica canale';
|
String get channels_editChannel => 'Modifica canale';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_muteChannel => 'Silenzia canale';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_unmuteChannel => 'Attiva notifiche canale';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_deleteChannel => 'Elimina canale';
|
String get channels_deleteChannel => 'Elimina canale';
|
||||||
|
|
||||||
@@ -789,6 +1121,11 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
return 'Eliminare \"$name\"? Non può essere annullato.';
|
return 'Eliminare \"$name\"? Non può essere annullato.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channels_channelDeleteFailed(String name) {
|
||||||
|
return 'Impossibile eliminare il canale \"$name\"';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String channels_channelDeleted(String name) {
|
String channels_channelDeleted(String name) {
|
||||||
return 'Canale \"$name\" eliminato';
|
return 'Canale \"$name\" eliminato';
|
||||||
@@ -902,6 +1239,14 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_noMessages => 'Nessun messaggio ancora';
|
String get chat_noMessages => 'Nessun messaggio ancora';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_sendMessage => 'Send message';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String chat_sendMessageTo(String contactName) {
|
||||||
|
return 'Invia un messaggio a $contactName';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_sendMessageToStart => 'Invia un messaggio per iniziare';
|
String get chat_sendMessageToStart => 'Invia un messaggio per iniziare';
|
||||||
|
|
||||||
@@ -921,11 +1266,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_location => 'Posizione';
|
String get chat_location => 'Posizione';
|
||||||
|
|
||||||
@override
|
|
||||||
String chat_sendMessageTo(String contactName) {
|
|
||||||
return 'Invia un messaggio a $contactName';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_typeMessage => 'Digita un messaggio...';
|
String get chat_typeMessage => 'Digita un messaggio...';
|
||||||
|
|
||||||
@@ -1077,6 +1417,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_pathManagement => 'Gestione Percorsi';
|
String get chat_pathManagement => 'Gestione Percorsi';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_ShowAllPaths => 'Mostra tutti i percorsi';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_routingMode => 'Modalità di routing';
|
String get chat_routingMode => 'Modalità di routing';
|
||||||
|
|
||||||
@@ -1236,6 +1579,12 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_title => 'Mappa Nodi';
|
String get map_title => 'Mappa Nodi';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_lineOfSight => 'Linea di vista';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_losScreenTitle => 'Linea di vista';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_noNodesWithLocation => 'Nessun nodo con dati di posizione';
|
String get map_noNodesWithLocation => 'Nessun nodo con dati di posizione';
|
||||||
|
|
||||||
@@ -1293,6 +1642,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';
|
||||||
|
|
||||||
@@ -1338,6 +1690,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';
|
||||||
|
|
||||||
@@ -1353,6 +1708,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';
|
||||||
|
|
||||||
@@ -1371,6 +1735,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';
|
||||||
|
|
||||||
@@ -1672,10 +2040,10 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Invia comandi al ripetitore';
|
String get repeater_cliSubtitle => 'Invia comandi al ripetitore';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Vicini';
|
String get repeater_neighbors => 'Vicini';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle =>
|
String get repeater_neighborsSubtitle =>
|
||||||
'Visualizza vicini di salto pari a zero.';
|
'Visualizza vicini di salto pari a zero.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -2061,6 +2429,12 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get repeater_cliQuickClock => 'Orologio';
|
String get repeater_cliQuickClock => 'Orologio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get repeater_cliQuickClockSync => 'Sincronizzazione dell\'orologio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get repeater_cliQuickDiscovery => 'Scopri i Vicini';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_cliHelpAdvert => 'Invia un pacchetto pubblicitario';
|
String get repeater_cliHelpAdvert => 'Invia un pacchetto pubblicitario';
|
||||||
|
|
||||||
@@ -2376,7 +2750,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Ripetitori Vicini';
|
String get neighbors_repeatersNeighbors => 'Ripetitori Vicini';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Nessun dato sugli vicini disponibile.';
|
String get neighbors_noData => 'Nessun dato sugli vicini disponibile.';
|
||||||
@@ -2685,6 +3059,15 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get listFilter_all => 'Tutti';
|
String get listFilter_all => 'Tutti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_favorites => 'Preferiti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_addToFavorites => 'Aggiungi ai preferiti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_removeFromFavorites => 'Rimuovi dai preferiti';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_users => 'Utenti';
|
String get listFilter_users => 'Utenti';
|
||||||
|
|
||||||
@@ -2720,6 +3103,145 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get pathTrace_clearTooltip => 'Pulisci percorso';
|
String get pathTrace_clearTooltip => 'Pulisci percorso';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectStartEnd =>
|
||||||
|
'Seleziona i nodi iniziali e finali per la LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losRunFailed(String error) {
|
||||||
|
return 'Controllo della linea di vista fallito: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losClearAllPoints => 'Cancella tutti i punti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRunToViewElevationProfile =>
|
||||||
|
'Eseguire LOS per visualizzare il profilo altimetrico';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuTitle => 'Menù LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuSubtitle =>
|
||||||
|
'Tocca i nodi o premi a lungo la mappa per punti personalizzati';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowDisplayNodes => 'Mostra i nodi di visualizzazione';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losCustomPoints => 'Punti personalizzati';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losCustomPointLabel(int index) {
|
||||||
|
return 'Personalizzato $index';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointA => 'Punto A';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointB => 'Punto B';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaA(String value, String unit) {
|
||||||
|
return 'Antenna A: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaB(String value, String unit) {
|
||||||
|
return 'Antenna B: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRun => 'Esegui LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losNoElevationData => 'Nessun dato di elevazione';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileClear(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String clearance,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, libera LOS, distanza minima $clearance $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileBlocked(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, bloccato da $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusChecking => 'LOS: controllo...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusNoData => 'LOS: nessun dato';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||||
|
return 'LOS: $clear/$total libera, $blocked bloccato, $unknown sconosciuto';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorElevationUnavailable =>
|
||||||
|
'Dati di elevazione non disponibili per uno o più campioni.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorInvalidInput =>
|
||||||
|
'Dati punti/elevazione non validi per il calcolo della LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRenameCustomPoint => 'Rinomina punto personalizzato';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointName => 'Nome del punto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowPanelTooltip => 'Mostra il pannello LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losHidePanelTooltip => 'Nascondi il pannello LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losElevationAttribution =>
|
||||||
|
'Dati di elevazione: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendRadioHorizon => 'Orizzonte radio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendLosBeam => 'Linea di vista';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendTerrain => 'Terreno';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyLabel => 'Frequenza';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyInfoTooltip => 'Visualizza i dettagli del calcolo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyDialogTitle => 'Calcolo dell’orizzonte radio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losFrequencyDialogDescription(
|
||||||
|
double baselineK,
|
||||||
|
double baselineFreq,
|
||||||
|
double frequencyMHz,
|
||||||
|
double kFactor,
|
||||||
|
) {
|
||||||
|
return 'Partendo da k=$baselineK a $baselineFreq MHz, il calcolo regola il fattore k per l\'attuale banda $frequencyMHz MHz, che definisce il limite curvo dell\'orizzonte radio.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_pathTrace => 'Traccia Percorso';
|
String get contacts_pathTrace => 'Traccia Percorso';
|
||||||
|
|
||||||
@@ -2893,4 +3415,281 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open esportazione dati mappa GPX';
|
'meshcore-open esportazione dati mappa GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Ripetitori vicini';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Ultimo accesso';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_title => 'Impostazioni dei contatti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddTitle => 'Scoperta automatica';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_otherTitle =>
|
||||||
|
'Altre impostazioni relative ai contatti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersTitle =>
|
||||||
|
'Aggiungere utenti automaticamente';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersSubtitle =>
|
||||||
|
'Consenti al compagno di aggiungere automaticamente gli utenti scoperti.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersTitle =>
|
||||||
|
'Aggiungere ripetitori automaticamente';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||||
|
'Consenti al compagno di aggiungere automaticamente i ripetitori scoperti.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersTitle =>
|
||||||
|
'Aggiungere automaticamente i server delle stanze';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||||
|
'Consenti al compagno di aggiungere automaticamente i server delle stanze scoperte.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsTitle =>
|
||||||
|
'Aggiungere automaticamente i sensori';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||||
|
'Consenti al compagno di aggiungere automaticamente i sensori scoperti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestTitle =>
|
||||||
|
'Sostituisci il più vecchio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestSubtitle =>
|
||||||
|
'Quando l\'elenco dei contatti è pieno, il contatto più vecchio non tra i preferiti verrà sostituito.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_Title => 'Contatti scoperti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_noMatching => 'Nessun contatto corrispondente';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_searchHint => 'Cerca contatti scoperti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_contactAdded => 'Contatto aggiunto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_addContact => 'Aggiungi contatto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_copyContact => 'Copia contatto negli appunti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContact => 'Elimina Contatto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAll =>
|
||||||
|
'Eliminare tutti i contatti scoperti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAllContent =>
|
||||||
|
'Sei sicuro di voler eliminare tutti i contatti scoperti?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_sendCooldown =>
|
||||||
|
'Si prega di attendere un momento prima di inviare nuovamente.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_jumpToOldestUnread =>
|
||||||
|
'Vai al messaggio più vecchio non letto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_jumpToOldestUnreadSubtitle =>
|
||||||
|
'Quando si apre una chat con messaggi non letti, scorrete verso l\'alto fino al primo messaggio non letto, invece che al più recente.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageHu => 'Ungherese';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageJa => 'Giapponese';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageKo => 'Coreano';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_tooltip => 'Statistiche per radio e reti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_screenTitle => 'Statistiche radio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_notConnected =>
|
||||||
|
'Connettiti a un dispositivo per visualizzare le statistiche radio.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_firmwareTooOld =>
|
||||||
|
'Le statistiche radio richiedono il firmware versione 8 o successiva.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_waiting => 'In attesa dei dati…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_noiseFloor(int noiseDbm) {
|
||||||
|
return 'Livello di rumore: $noiseDbm dBm';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_lastRssi(int rssiDbm) {
|
||||||
|
return 'Ultimo valore RSSI: $rssiDbm dBm';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_lastSnr(String snr) {
|
||||||
|
return 'Ultimo SNR: $snr dB';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_txAir(int seconds) {
|
||||||
|
return 'Tempo di trasmissione in diretta (totale): $seconds s';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_rxAir(int seconds) {
|
||||||
|
return 'Tempo di trasmissione RX (totale): $seconds s';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_chartCaption =>
|
||||||
|
'Livello di rumore (dBm) misurato su campioni recenti.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_stripNoise(int noiseDbm) {
|
||||||
|
return 'Livello di rumore: $noiseDbm dBm';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_stripWaiting => 'Recupero delle statistiche radio…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_settingsTile => 'Statistiche radio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_settingsSubtitle =>
|
||||||
|
'Livello di rumore, RSSI, rapporto segnale/rumore (SNR) e tempo di trasmissione';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_title => 'Traduzione';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_enableTitle => 'Abilitare la traduzione';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_enableSubtitle =>
|
||||||
|
'Tradurre i messaggi in arrivo e consentire la traduzione preventiva prima dell\'invio.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerTitle => 'Tradurre prima di inviare';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerSubtitle =>
|
||||||
|
'Controlla lo stato predefinito dell\'icona di traduzione del compositore.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_targetLanguage => 'Lingua di destinazione';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_useAppLanguage => 'Utilizza la lingua dell\'app';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadedModelLabel => 'Modello scaricato';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_presetModelLabel =>
|
||||||
|
'Modello predefinito di Hugging Face';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_manualUrlLabel => 'URL del modello manuale';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadModel => 'Scarica il modello';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloading => 'Inizio download...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_working => 'Lavoro...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_stop => 'Smetta';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_mergingChunks =>
|
||||||
|
'Unione dei frammenti scaricati in un unico file...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadedModels => 'Modelli scaricati';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_deleteModel => 'Elimina modello';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_modelDownloaded => 'Modello di traduzione scaricato.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadStopped => 'Il download è stato interrotto.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String translation_downloadFailed(String error) {
|
||||||
|
return 'Download fallito: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_enterUrlFirst =>
|
||||||
|
'Inserite innanzitutto l\'URL del modello.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_linuxPairingShowPin => 'Mostra PIN';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_linuxPairingHidePin => 'Nascondi il PIN';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_linuxPairingPinTitle =>
|
||||||
|
'PIN per l\'accoppiamento Bluetooth';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||||
|
return 'Inserire il codice PIN per $deviceName (lasciare vuoto se non presente).';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_messageTranslation => 'Traduzione del messaggio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_translateBeforeSending => 'Tradurre prima di inviare';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerEnabledHint =>
|
||||||
|
'I messaggi verranno tradotti prima di essere inviati.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerDisabledHint =>
|
||||||
|
'Invia messaggi utilizzando la lingua originale, scritta.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String translation_translateTo(String language) {
|
||||||
|
return 'Tradurre in $language';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_translationOptions => 'Opzioni di traduzione';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_systemLanguage => 'Lingua del sistema';
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1030
-225
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';
|
||||||
|
|
||||||
@@ -324,6 +516,10 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
String get settings_aboutDescription =>
|
String get settings_aboutDescription =>
|
||||||
'Um cliente Flutter de código aberto para dispositivos de rede mesh LoRa Core da MeshCore.';
|
'Um cliente Flutter de código aberto para dispositivos de rede mesh LoRa Core da MeshCore.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_aboutOpenMeteoAttribution =>
|
||||||
|
'Dados de elevação LOS: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_infoName => 'Nome';
|
String get settings_infoName => 'Nome';
|
||||||
|
|
||||||
@@ -460,6 +656,14 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageUk => 'Ucraniano';
|
String get appSettings_languageUk => 'Ucraniano';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracing =>
|
||||||
|
'Ativar rastreamento de mensagens';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracingSubtitle =>
|
||||||
|
'Mostrar metadados detalhados de roteamento e tempo para as mensagens';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Notificações';
|
String get appSettings_notifications => 'Notificações';
|
||||||
|
|
||||||
@@ -537,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';
|
||||||
|
|
||||||
@@ -620,6 +867,15 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_offlineMapCache => 'Cache de Mapa Offline';
|
String get appSettings_offlineMapCache => 'Cache de Mapa Offline';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsTitle => 'Unidades';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsMetric => 'Métrico (m/km)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsImperial => 'Imperial (ft/mi)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_noAreaSelected => 'Nenhuma área selecionada';
|
String get appSettings_noAreaSelected => 'Nenhuma área selecionada';
|
||||||
|
|
||||||
@@ -658,7 +914,35 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
'Os contatos serão exibidos quando os dispositivos anunciarem.';
|
'Os contatos serão exibidos quando os dispositivos anunciarem.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_searchContacts => 'Pesquisar contatos...';
|
String get contacts_unread => 'Não lido';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_searchContactsNoNumber => 'Pesquisar Contatos...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchContacts(int number, String str) {
|
||||||
|
return 'Pesquisar contatos...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchFavorites(int number, String str) {
|
||||||
|
return 'Pesquisar $number$str Favoritos...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchUsers(int number, String str) {
|
||||||
|
return 'Pesquisar $number$str Usuários...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRepeaters(int number, String str) {
|
||||||
|
return 'Pesquisar $number$str Repetidores...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRoomServers(int number, String str) {
|
||||||
|
return 'Pesquisar $number$str servidores de sala...';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_noUnreadContacts => 'Sem contatos não lidos.';
|
String get contacts_noUnreadContacts => 'Sem contatos não lidos.';
|
||||||
@@ -707,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';
|
||||||
@@ -746,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';
|
||||||
|
|
||||||
@@ -784,6 +1107,12 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_editChannel => 'Editar canal';
|
String get channels_editChannel => 'Editar canal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_muteChannel => 'Silenciar canal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_unmuteChannel => 'Ativar canal';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_deleteChannel => 'Excluir canal';
|
String get channels_deleteChannel => 'Excluir canal';
|
||||||
|
|
||||||
@@ -792,6 +1121,11 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
return 'Excluir \"$name\"? Não pode ser desfeito.';
|
return 'Excluir \"$name\"? Não pode ser desfeito.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channels_channelDeleteFailed(String name) {
|
||||||
|
return 'Falha ao excluir o canal \"$name\"';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String channels_channelDeleted(String name) {
|
String channels_channelDeleted(String name) {
|
||||||
return 'Canal \"$name\" excluído';
|
return 'Canal \"$name\" excluído';
|
||||||
@@ -904,6 +1238,14 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_noMessages => 'Ainda não existem mensagens.';
|
String get chat_noMessages => 'Ainda não existem mensagens.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_sendMessage => 'Send message';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String chat_sendMessageTo(String contactName) {
|
||||||
|
return 'Enviar uma mensagem para $contactName';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_sendMessageToStart => 'Enviar uma mensagem para começar';
|
String get chat_sendMessageToStart => 'Enviar uma mensagem para começar';
|
||||||
|
|
||||||
@@ -923,11 +1265,6 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_location => 'Localização';
|
String get chat_location => 'Localização';
|
||||||
|
|
||||||
@override
|
|
||||||
String chat_sendMessageTo(String contactName) {
|
|
||||||
return 'Enviar uma mensagem para $contactName';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_typeMessage => 'Digite uma mensagem...';
|
String get chat_typeMessage => 'Digite uma mensagem...';
|
||||||
|
|
||||||
@@ -1079,6 +1416,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_pathManagement => 'Gerenciamento de Caminhos';
|
String get chat_pathManagement => 'Gerenciamento de Caminhos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_ShowAllPaths => 'Mostrar todos os caminhos';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_routingMode => 'Modo de roteamento';
|
String get chat_routingMode => 'Modo de roteamento';
|
||||||
|
|
||||||
@@ -1237,6 +1577,12 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_title => 'Mapa de Nós';
|
String get map_title => 'Mapa de Nós';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_lineOfSight => 'Linha de visão';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_losScreenTitle => 'Linha de visão';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_noNodesWithLocation =>
|
String get map_noNodesWithLocation =>
|
||||||
'Não existem nós com dados de localização.';
|
'Não existem nós com dados de localização.';
|
||||||
@@ -1295,6 +1641,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';
|
||||||
|
|
||||||
@@ -1340,6 +1689,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';
|
||||||
|
|
||||||
@@ -1355,6 +1707,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';
|
||||||
|
|
||||||
@@ -1373,6 +1735,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';
|
||||||
|
|
||||||
@@ -1674,11 +2039,10 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Enviar comandos ao repetidor';
|
String get repeater_cliSubtitle => 'Enviar comandos ao repetidor';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Vizinhos';
|
String get repeater_neighbors => 'Vizinhos';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle =>
|
String get repeater_neighborsSubtitle => 'Visualizar vizinhos de salto zero.';
|
||||||
'Visualizar vizinhos de salto zero.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => 'Configurações';
|
String get repeater_settings => 'Configurações';
|
||||||
@@ -2062,6 +2426,12 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get repeater_cliQuickClock => 'Relógio';
|
String get repeater_cliQuickClock => 'Relógio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get repeater_cliQuickClockSync => 'Sincronização do Relógio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get repeater_cliQuickDiscovery => 'Descobrir Vizinhos';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_cliHelpAdvert => 'Envia um pacote de anúncios';
|
String get repeater_cliHelpAdvert => 'Envia um pacote de anúncios';
|
||||||
|
|
||||||
@@ -2377,7 +2747,7 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Repetidores Vizinhos';
|
String get neighbors_repeatersNeighbors => 'Repetidores Vizinhos';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Não estão disponíveis dados de vizinhos.';
|
String get neighbors_noData => 'Não estão disponíveis dados de vizinhos.';
|
||||||
@@ -2687,6 +3057,15 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get listFilter_all => 'Tudo';
|
String get listFilter_all => 'Tudo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_favorites => 'Favoritos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_addToFavorites => 'Adicionar aos favoritos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_removeFromFavorites => 'Remover da lista de favoritos';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_users => 'Usuários';
|
String get listFilter_users => 'Usuários';
|
||||||
|
|
||||||
@@ -2721,6 +3100,144 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get pathTrace_clearTooltip => 'Limpar caminho';
|
String get pathTrace_clearTooltip => 'Limpar caminho';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectStartEnd => 'Selecione nós iniciais e finais para LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losRunFailed(String error) {
|
||||||
|
return 'Falha na verificação da linha de visão: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losClearAllPoints => 'Limpe todos os pontos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRunToViewElevationProfile =>
|
||||||
|
'Execute o LOS para visualizar o perfil de elevação';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuTitle => 'Menu LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuSubtitle =>
|
||||||
|
'Toque nos nós ou mantenha pressionado o mapa para obter pontos personalizados';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowDisplayNodes => 'Mostrar nós de exibição';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losCustomPoints => 'Pontos personalizados';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losCustomPointLabel(int index) {
|
||||||
|
return '$index personalizado';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointA => 'Ponto A';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointB => 'Ponto B';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaA(String value, String unit) {
|
||||||
|
return 'Antena A: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaB(String value, String unit) {
|
||||||
|
return 'Antena B: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRun => 'Executar LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losNoElevationData => 'Sem dados de elevação';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileClear(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String clearance,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, limpar LOS, liberação mínima $clearance $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileBlocked(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, bloqueado por $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusChecking => 'LOS: verificando...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusNoData => 'LOS: sem dados';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||||
|
return 'LOS: $clear/$total limpo, $blocked bloqueado, $unknown desconhecido';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorElevationUnavailable =>
|
||||||
|
'Dados de elevação indisponíveis para uma ou mais amostras.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorInvalidInput =>
|
||||||
|
'Dados de pontos/elevação inválidos para cálculo de LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRenameCustomPoint => 'Renomear ponto personalizado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointName => 'Nome do ponto';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowPanelTooltip => 'Mostrar painel LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losHidePanelTooltip => 'Ocultar painel LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losElevationAttribution =>
|
||||||
|
'Dados de elevação: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendRadioHorizon => 'Horizonte de rádio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendLosBeam => 'Linha de visada';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendTerrain => 'Terreno';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyLabel => 'Frequência';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyInfoTooltip => 'Ver detalhes do cálculo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyDialogTitle => 'Cálculo do horizonte de rádio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losFrequencyDialogDescription(
|
||||||
|
double baselineK,
|
||||||
|
double baselineFreq,
|
||||||
|
double frequencyMHz,
|
||||||
|
double kFactor,
|
||||||
|
) {
|
||||||
|
return 'Começando em k=$baselineK em $baselineFreq MHz, o cálculo ajusta o fator k para a banda atual de $frequencyMHz MHz, que define o limite do horizonte de rádio curvo.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_pathTrace => 'Traçado de Caminho';
|
String get contacts_pathTrace => 'Traçado de Caminho';
|
||||||
|
|
||||||
@@ -2890,4 +3407,280 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open exportação de dados de mapa GPX';
|
'meshcore-open exportação de dados de mapa GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Repetidores Próximos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Visto pela última vez';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_title => 'Configurações de contatos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddTitle => 'Descoberta Automática';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_otherTitle =>
|
||||||
|
'Outras configurações relacionadas a contatos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersTitle =>
|
||||||
|
'Adicionar usuários automaticamente';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersSubtitle =>
|
||||||
|
'Permitir que o companheiro adicione automaticamente os usuários descobertos.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersTitle =>
|
||||||
|
'Adicionar repetidores automaticamente';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||||
|
'Permitir que o companheiro adicione automaticamente os repetidores descobertos.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersTitle =>
|
||||||
|
'Adicionar automaticamente servidores de sala';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||||
|
'Permitir que o companheiro adicione automaticamente os servidores de salas descobertos.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsTitle =>
|
||||||
|
'Adicionar sensores automaticamente';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||||
|
'Permitir que o companheiro adicione automaticamente sensores descobertos.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestTitle =>
|
||||||
|
'Sobrescrever o Mais Antigo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestSubtitle =>
|
||||||
|
'Quando a lista de contatos estiver cheia, o contato mais antigo não favoritado será substituído.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_Title => 'Contatos Descobertos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_noMatching => 'Nenhum contato correspondente';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_searchHint => 'Pesquisar contatos descobertos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_contactAdded => 'Contato adicionado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_addContact => 'Adicionar Contato';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_copyContact =>
|
||||||
|
'Copiar Contato para a área de transferência';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContact => 'Excluir Contato';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAll =>
|
||||||
|
'Excluir Todos os Contatos Descobertos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAllContent =>
|
||||||
|
'Tem certeza de que deseja excluir todos os contatos descobertos?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_sendCooldown =>
|
||||||
|
'Por favor, aguarde um momento antes de reenviar.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_jumpToOldestUnread =>
|
||||||
|
'Vá para a mensagem mais antiga não lida';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_jumpToOldestUnreadSubtitle =>
|
||||||
|
'Ao abrir uma conversa com mensagens não lidas, role para a primeira mensagem não lida, em vez da mais recente.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageHu => 'Húngaro';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageJa => 'Japonês';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageKo => 'Coreano';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_tooltip => 'Estatísticas de rádio e malha';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_screenTitle => 'Estatísticas de rádio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_notConnected =>
|
||||||
|
'Conecte-se a um dispositivo para visualizar estatísticas de rádio.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_firmwareTooOld =>
|
||||||
|
'As estatísticas de rádio exigem o firmware v8 ou uma versão mais recente.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_waiting => 'Aguardando dados…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_noiseFloor(int noiseDbm) {
|
||||||
|
return 'Nível de ruído: $noiseDbm dBm';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_lastRssi(int rssiDbm) {
|
||||||
|
return 'Último RSSI: $rssiDbm dBm';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_lastSnr(String snr) {
|
||||||
|
return 'Último SNR: $snr dB';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_txAir(int seconds) {
|
||||||
|
return 'Tempo de transmissão da TX (total): $seconds s';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_rxAir(int seconds) {
|
||||||
|
return 'Tempo de uso do RX (total): $seconds s';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_chartCaption =>
|
||||||
|
'Nível de ruído (dBm) em amostras recentes.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_stripNoise(int noiseDbm) {
|
||||||
|
return 'Nível de ruído: $noiseDbm dBm';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_stripWaiting => 'Obtendo estatísticas de rádio…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_settingsTile => 'Estatísticas de rádio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_settingsSubtitle =>
|
||||||
|
'Nível de ruído, RSSI, SNR e tempo de transmissão';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_title => 'Tradução';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_enableTitle => 'Ativar a tradução';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_enableSubtitle =>
|
||||||
|
'Traduzir mensagens recebidas e permitir a tradução antes do envio.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerTitle => 'Traduza antes de enviar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerSubtitle =>
|
||||||
|
'Controla o estado padrão do ícone de tradução do compositor.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_targetLanguage => 'Língua-alvo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_useAppLanguage => 'Utilize o idioma da aplicação';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadedModelLabel => 'Modelo baixado';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_presetModelLabel =>
|
||||||
|
'Modelo pré-definido da Hugging Face';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_manualUrlLabel => 'URL do modelo manual';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadModel => 'Baixar modelo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloading => 'Baixando...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_working => 'Trabalhando...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_stop => 'Pare';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_mergingChunks =>
|
||||||
|
'Combinando os fragmentos baixados em um único arquivo...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadedModels => 'Modelos baixados';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_deleteModel => 'Excluir modelo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_modelDownloaded => 'Modelo de tradução baixado.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadStopped => 'Download interrompido.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String translation_downloadFailed(String error) {
|
||||||
|
return 'Falha na descarga: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_enterUrlFirst => 'Insira primeiro a URL do modelo.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_linuxPairingShowPin => 'Mostrar PIN';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_linuxPairingHidePin => 'Ocultar PIN';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_linuxPairingPinTitle => 'PIN de emparelhamento Bluetooth';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||||
|
return 'Insira o PIN para $deviceName (deixe em branco se não houver).';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_messageTranslation => 'Tradução da mensagem';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_translateBeforeSending => 'Traduzir antes de enviar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerEnabledHint =>
|
||||||
|
'As mensagens serão traduzidas antes de serem enviadas.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerDisabledHint =>
|
||||||
|
'Envie mensagens no idioma original, conforme digitado.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String translation_translateTo(String language) {
|
||||||
|
return 'Traduzir para $language';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_translationOptions => 'Opções de tradução';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_systemLanguage => 'Idioma do sistema';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 => 'Действия';
|
||||||
|
|
||||||
@@ -321,6 +514,10 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
String get settings_aboutDescription =>
|
String get settings_aboutDescription =>
|
||||||
'Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.';
|
'Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_aboutOpenMeteoAttribution =>
|
||||||
|
'Данные о высоте LOS: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_infoName => 'Имя';
|
String get settings_infoName => 'Имя';
|
||||||
|
|
||||||
@@ -458,6 +655,14 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageUk => 'Українська';
|
String get appSettings_languageUk => 'Українська';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracing =>
|
||||||
|
'Включить трассировку сообщений';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracingSubtitle =>
|
||||||
|
'Показывать подробные метаданные о маршрутизации и времени для сообщений';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Уведомления';
|
String get appSettings_notifications => 'Уведомления';
|
||||||
|
|
||||||
@@ -536,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 => 'Батарея';
|
||||||
|
|
||||||
@@ -620,6 +869,15 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_offlineMapCache => 'Кэш офлайн-карты';
|
String get appSettings_offlineMapCache => 'Кэш офлайн-карты';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsTitle => 'Единицы';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsMetric => 'Метрическая (м/км)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsImperial => 'Имперская (ft / mi)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_noAreaSelected => 'Область не выбрана';
|
String get appSettings_noAreaSelected => 'Область не выбрана';
|
||||||
|
|
||||||
@@ -657,7 +915,35 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
'Контакты появятся, когда устройства начнут рассылать оповещения';
|
'Контакты появятся, когда устройства начнут рассылать оповещения';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_searchContacts => 'Поиск контактов...';
|
String get contacts_unread => 'Непрочитанное';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_searchContactsNoNumber => 'Поиск контактов...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchContacts(int number, String str) {
|
||||||
|
return 'Поиск контактов...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchFavorites(int number, String str) {
|
||||||
|
return 'Поиск $number$str избранного...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchUsers(int number, String str) {
|
||||||
|
return 'Поиск $number$str пользователей...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRepeaters(int number, String str) {
|
||||||
|
return 'Поиск $number$str ретрансляторов...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRoomServers(int number, String str) {
|
||||||
|
return 'Поиск $number$str серверов комнат...';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_noUnreadContacts => 'Нет непрочитанных контактов';
|
String get contacts_noUnreadContacts => 'Нет непрочитанных контактов';
|
||||||
@@ -705,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\" уже существует';
|
||||||
@@ -744,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 => 'Каналы';
|
||||||
|
|
||||||
@@ -782,6 +1107,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_editChannel => 'Изменить канал';
|
String get channels_editChannel => 'Изменить канал';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_muteChannel => 'Отключить уведомления канала';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_unmuteChannel => 'Включить уведомления канала';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_deleteChannel => 'Удалить канал';
|
String get channels_deleteChannel => 'Удалить канал';
|
||||||
|
|
||||||
@@ -790,6 +1121,11 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
return 'Удалить \"$name\"? Это действие нельзя отменить.';
|
return 'Удалить \"$name\"? Это действие нельзя отменить.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channels_channelDeleteFailed(String name) {
|
||||||
|
return 'Не удалось удалить канал $name.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String channels_channelDeleted(String name) {
|
String channels_channelDeleted(String name) {
|
||||||
return 'Канал \"$name\" удалён';
|
return 'Канал \"$name\" удалён';
|
||||||
@@ -902,6 +1238,14 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_noMessages => 'Сообщений пока нет';
|
String get chat_noMessages => 'Сообщений пока нет';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_sendMessage => 'Send message';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String chat_sendMessageTo(String contactName) {
|
||||||
|
return 'Отправить сообщение $contactName';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_sendMessageToStart => 'Отправьте сообщение, чтобы начать';
|
String get chat_sendMessageToStart => 'Отправьте сообщение, чтобы начать';
|
||||||
|
|
||||||
@@ -921,11 +1265,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_location => 'Местоположение';
|
String get chat_location => 'Местоположение';
|
||||||
|
|
||||||
@override
|
|
||||||
String chat_sendMessageTo(String contactName) {
|
|
||||||
return 'Отправить сообщение $contactName';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_typeMessage => 'Напишите сообщение...';
|
String get chat_typeMessage => 'Напишите сообщение...';
|
||||||
|
|
||||||
@@ -1077,6 +1416,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_pathManagement => 'Управление маршрутами';
|
String get chat_pathManagement => 'Управление маршрутами';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_ShowAllPaths => 'Показать все пути';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_routingMode => 'Режим маршрутизации';
|
String get chat_routingMode => 'Режим маршрутизации';
|
||||||
|
|
||||||
@@ -1239,6 +1581,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_title => 'Карта нод';
|
String get map_title => 'Карта нод';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_lineOfSight => 'Линия видимости';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_losScreenTitle => 'Линия видимости';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_noNodesWithLocation => 'Нет нод с данными о местоположении';
|
String get map_noNodesWithLocation => 'Нет нод с данными о местоположении';
|
||||||
|
|
||||||
@@ -1296,6 +1644,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 => 'Метка';
|
||||||
|
|
||||||
@@ -1341,6 +1692,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 => 'Префикс ключа';
|
||||||
|
|
||||||
@@ -1356,6 +1710,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 => 'Время последнего появления';
|
||||||
|
|
||||||
@@ -1374,6 +1738,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 => 'Удалить последний';
|
||||||
|
|
||||||
@@ -1676,10 +2043,10 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Отправка команд репитеру';
|
String get repeater_cliSubtitle => 'Отправка команд репитеру';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Соседи';
|
String get repeater_neighbors => 'Соседи';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle => 'Просмотр соседей на нулевом хопе.';
|
String get repeater_neighborsSubtitle => 'Просмотр соседей на нулевом хопе.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => 'Настройки';
|
String get repeater_settings => 'Настройки';
|
||||||
@@ -2063,6 +2430,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get repeater_cliQuickClock => 'Время';
|
String get repeater_cliQuickClock => 'Время';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get repeater_cliQuickClockSync => 'Синхронизация часов';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get repeater_cliQuickDiscovery => 'Обнаружить Соседей';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_cliHelpAdvert => 'Отправляет пакет анонсирования';
|
String get repeater_cliHelpAdvert => 'Отправляет пакет анонсирования';
|
||||||
|
|
||||||
@@ -2379,7 +2752,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Соседи репитеров';
|
String get neighbors_repeatersNeighbors => 'Соседи репитеров';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Данные о соседях недоступны.';
|
String get neighbors_noData => 'Данные о соседях недоступны.';
|
||||||
@@ -2689,6 +3062,15 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get listFilter_all => 'Все';
|
String get listFilter_all => 'Все';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_favorites => 'Избранное';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_addToFavorites => 'Добавить в избранное';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_removeFromFavorites => 'Удалить из избранного';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_users => 'Пользователи';
|
String get listFilter_users => 'Пользователи';
|
||||||
|
|
||||||
@@ -2723,6 +3105,144 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get pathTrace_clearTooltip => 'Очистить путь';
|
String get pathTrace_clearTooltip => 'Очистить путь';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectStartEnd => 'Выберите начальный и конечный узлы для LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losRunFailed(String error) {
|
||||||
|
return 'Проверка прямой видимости не удалась: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losClearAllPoints => 'Очистить все точки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRunToViewElevationProfile =>
|
||||||
|
'Запустите LOS, чтобы просмотреть профиль высот.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuTitle => 'ЛОС Меню';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuSubtitle =>
|
||||||
|
'Коснитесь узлов или нажмите и удерживайте карту для выбора пользовательских точек.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowDisplayNodes => 'Показать узлы отображения';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losCustomPoints => 'Пользовательские точки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losCustomPointLabel(int index) {
|
||||||
|
return 'Пользовательский $index';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointA => 'Точка А';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointB => 'Точка Б';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaA(String value, String unit) {
|
||||||
|
return 'Антенна А: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaB(String value, String unit) {
|
||||||
|
return 'Антенна Б: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRun => 'Запустить ЛОС';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losNoElevationData => 'Нет данных о высоте';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileClear(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String clearance,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, свободная зона видимости, минимальный зазор $clearance $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileBlocked(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, заблокирован $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusChecking => 'ЛОС: проверяю...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusNoData => 'ЛОС: нет данных';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||||
|
return 'LOS: $clear/$total очищено, $blocked заблокировано, $unknown неизвестно.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorElevationUnavailable =>
|
||||||
|
'Данные о высоте недоступны для одного или нескольких образцов.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorInvalidInput =>
|
||||||
|
'Неверные данные о точках/высоте для расчета LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRenameCustomPoint => 'Переименовать пользовательскую точку';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointName => 'Имя точки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowPanelTooltip => 'Показать панель LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losHidePanelTooltip => 'Скрыть панель LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losElevationAttribution =>
|
||||||
|
'Данные о высоте: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendRadioHorizon => 'Радиогоризонт';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendLosBeam => 'Линия прямой видимости';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendTerrain => 'Рельеф';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyLabel => 'Частота';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyInfoTooltip => 'Просмотреть детали расчёта';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyDialogTitle => 'Расчёт радиогоризонта';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losFrequencyDialogDescription(
|
||||||
|
double baselineK,
|
||||||
|
double baselineFreq,
|
||||||
|
double frequencyMHz,
|
||||||
|
double kFactor,
|
||||||
|
) {
|
||||||
|
return 'Начиная с k=$baselineK на частоте $baselineFreq МГц, расчет корректирует коэффициент k для текущего диапазона $frequencyMHz МГц, который определяет изогнутую границу радиогоризонта.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_pathTrace => 'Трассировка пути';
|
String get contacts_pathTrace => 'Трассировка пути';
|
||||||
|
|
||||||
@@ -2901,4 +3421,280 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open экспорт данных карты GPX';
|
'meshcore-open экспорт данных карты GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Ближайшие ретрансляторы';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Последний раз видели';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_title => 'Настройки контактов';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddTitle => 'Автоматическое обнаружение';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_otherTitle =>
|
||||||
|
'Другие настройки, связанные с контактами';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersTitle =>
|
||||||
|
'Автоматически добавлять пользователей';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersSubtitle =>
|
||||||
|
'Разрешить компаньону автоматически добавлять обнаруженных пользователей';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersTitle =>
|
||||||
|
'Автоматически добавлять ретрансляторы';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||||
|
'Разрешить спутнику автоматически добавлять обнаруженные ретрансляторы';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersTitle =>
|
||||||
|
'Автоматически добавлять серверы комнат';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||||
|
'Разрешить компаньону автоматически добавлять обнаруженные сервера комнат.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsTitle =>
|
||||||
|
'Автоматически добавлять датчики';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||||
|
'Разрешить компаньону автоматически добавлять обнаруженные датчики';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestTitle =>
|
||||||
|
'Перезаписать самое старое';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestSubtitle =>
|
||||||
|
'Когда список контактов заполнен, будет заменен самый старый контакт, который не находится в избранном.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_Title => 'Обнаруженные контакты';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_noMatching => 'Нет совпадающих контактов';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_searchHint => 'Найденные контакты поиска';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_contactAdded => 'Контакт добавлен';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_addContact => 'Добавить контакт';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_copyContact =>
|
||||||
|
'Копировать контакт в буфер обмена';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContact => 'Удалить контакт';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAll =>
|
||||||
|
'Удалить Все Обнаруженные Контакты';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAllContent =>
|
||||||
|
'Вы уверены, что хотите удалить все обнаруженные контакты?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_sendCooldown =>
|
||||||
|
'Пожалуйста, подождите немного, прежде чем отправлять сообщение снова.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_jumpToOldestUnread =>
|
||||||
|
'Перейти к самому старому непрочитанному сообщению';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_jumpToOldestUnreadSubtitle =>
|
||||||
|
'При открытии чата с непрочитанными сообщениями, прокрутите страницу, чтобы увидеть первое непрочитанное сообщение, а не последнее.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageHu => 'Венгерский';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageJa => 'Японский';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageKo => 'Корейский';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_tooltip => 'Статистика радио и беспроводной сети';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_screenTitle => 'Статистика радиовещания';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_notConnected =>
|
||||||
|
'Подключитесь к устройству, чтобы просмотреть статистику радио.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_firmwareTooOld =>
|
||||||
|
'Для работы радиостатистики требуется установленная версия прошивки v8 или более новая.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_waiting => 'Ожидаем данных…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_noiseFloor(int noiseDbm) {
|
||||||
|
return 'Уровень шума: $noiseDbm дБм';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_lastRssi(int rssiDbm) {
|
||||||
|
return 'Последнее значение RSSI: $rssiDbm дБм';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_lastSnr(String snr) {
|
||||||
|
return 'Последнее значение SNR: $snr дБ';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_txAir(int seconds) {
|
||||||
|
return 'Время эфира на телеканале TX (общее): $seconds секунд';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_rxAir(int seconds) {
|
||||||
|
return 'Общее время использования RX (в секундах): $seconds с';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_chartCaption =>
|
||||||
|
'Уровень шума (дБм) на основе последних измерений.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_stripNoise(int noiseDbm) {
|
||||||
|
return 'Уровень шума: $noiseDbm дБм';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_stripWaiting => 'Получение данных о радио…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_settingsTile => 'Статистика радиовещания';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_settingsSubtitle =>
|
||||||
|
'Уровень шума, RSSI, SNR и время передачи';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_title => 'Перевод';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_enableTitle => 'Включить перевод';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_enableSubtitle =>
|
||||||
|
'Переводить входящие сообщения и позволять предварительный перевод перед отправкой.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerTitle => 'Переводить перед отправкой';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerSubtitle =>
|
||||||
|
'Управляет исходным состоянием значка перевода, предоставляемого редактором.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_targetLanguage => 'Целевой язык';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_useAppLanguage => 'Используйте язык приложения';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadedModelLabel => 'Загруженная модель';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_presetModelLabel =>
|
||||||
|
'Предопределенная модель от Hugging Face';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_manualUrlLabel => 'Ссылка на руководство';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadModel => 'Скачать модель';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloading => 'Загрузка...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_working => 'Работа...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_stop => 'Прекратите';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_mergingChunks =>
|
||||||
|
'Объединение скачанных фрагментов в один финальный файл...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadedModels => 'Загруженные модели';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_deleteModel => 'Удалить модель';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_modelDownloaded => 'Модель перевода загружена.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadStopped => 'Процесс загрузки был прерван.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String translation_downloadFailed(String error) {
|
||||||
|
return 'Не удалось скачать: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_enterUrlFirst => 'Сначала введите URL модели.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_linuxPairingShowPin => 'Показать PIN';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_linuxPairingHidePin => 'Скрыть PIN';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_linuxPairingPinTitle => 'PIN‑код сопряжения Bluetooth';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||||
|
return 'Введите PIN‑код для $deviceName (оставьте пустым, если нет).';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_messageTranslation => 'Перевод сообщения';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_translateBeforeSending => 'Перевести перед отправкой';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerEnabledHint =>
|
||||||
|
'Сообщения будут переведены перед отправкой.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerDisabledHint =>
|
||||||
|
'Отправляйте сообщения на языке, в котором они были изначально набраны.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String translation_translateTo(String language) {
|
||||||
|
return 'Перевести на $language';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_translationOptions => 'Варианты перевода';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_systemLanguage => 'Язык системы';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
@@ -320,6 +509,10 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
String get settings_aboutDescription =>
|
String get settings_aboutDescription =>
|
||||||
'Otvorený zdrojový Flutter klient pre MeshCore LoRa sieťové zariadenia.';
|
'Otvorený zdrojový Flutter klient pre MeshCore LoRa sieťové zariadenia.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_aboutOpenMeteoAttribution =>
|
||||||
|
'Údaje o nadmorskej výške LOS: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_infoName => 'Meno';
|
String get settings_infoName => 'Meno';
|
||||||
|
|
||||||
@@ -456,6 +649,13 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageUk => 'Ukrajinská';
|
String get appSettings_languageUk => 'Ukrajinská';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracing => 'Povoliť sledovanie správ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracingSubtitle =>
|
||||||
|
'Zobraziť podrobné metadáta o smerovaní a časovaní správ';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Upozornenia';
|
String get appSettings_notifications => 'Upozornenia';
|
||||||
|
|
||||||
@@ -530,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';
|
||||||
|
|
||||||
@@ -614,6 +856,15 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_offlineMapCache => 'Offline Mapa Pamäť';
|
String get appSettings_offlineMapCache => 'Offline Mapa Pamäť';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsTitle => 'Jednotky';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsMetric => 'Metrické (m / km)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsImperial => 'Imperiálne (ft / mi)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_noAreaSelected => 'Neoznačila sa žiadna oblasť';
|
String get appSettings_noAreaSelected => 'Neoznačila sa žiadna oblasť';
|
||||||
|
|
||||||
@@ -651,7 +902,35 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
'Kontakty sa zobrazia, keď zariadenia spúšťajú reklamu.';
|
'Kontakty sa zobrazia, keď zariadenia spúšťajú reklamu.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_searchContacts => 'Vyhľadávajte kontakty...';
|
String get contacts_unread => 'Neprečítané';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_searchContactsNoNumber => 'Hľadať kontakty...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchContacts(int number, String str) {
|
||||||
|
return 'Vyhľadávajte kontakty...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchFavorites(int number, String str) {
|
||||||
|
return 'Hľadať $number$str obľúbené...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchUsers(int number, String str) {
|
||||||
|
return 'Hľadať $number$str používateľov...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRepeaters(int number, String str) {
|
||||||
|
return 'Hľadať $number$str opakovače...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRoomServers(int number, String str) {
|
||||||
|
return 'Hľadaj $number$str serverov miestností...';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_noUnreadContacts => 'Žiadne neprečítané kontakty';
|
String get contacts_noUnreadContacts => 'Žiadne neprečítané kontakty';
|
||||||
@@ -700,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';
|
||||||
@@ -741,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';
|
||||||
|
|
||||||
@@ -779,6 +1096,12 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_editChannel => 'Upraviť kanál';
|
String get channels_editChannel => 'Upraviť kanál';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_muteChannel => 'Stlmiť kanál';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_unmuteChannel => 'Zrušiť stlmenie kanála';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_deleteChannel => 'Odstrániť kanál';
|
String get channels_deleteChannel => 'Odstrániť kanál';
|
||||||
|
|
||||||
@@ -787,6 +1110,11 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
return 'Odstrániť \"$name\"? To sa nedá zrušiť.';
|
return 'Odstrániť \"$name\"? To sa nedá zrušiť.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channels_channelDeleteFailed(String name) {
|
||||||
|
return 'Kanál \"$name\" sa nepodarilo odstrániť';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String channels_channelDeleted(String name) {
|
String channels_channelDeleted(String name) {
|
||||||
return 'Kanál \"$name\" bol odstránený';
|
return 'Kanál \"$name\" bol odstránený';
|
||||||
@@ -898,6 +1226,14 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_noMessages => 'Zatiaľ žiadne správy.';
|
String get chat_noMessages => 'Zatiaľ žiadne správy.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_sendMessage => 'Send message';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String chat_sendMessageTo(String contactName) {
|
||||||
|
return 'Pošli správu $contactName';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_sendMessageToStart => 'Pošlite správu na začiatok';
|
String get chat_sendMessageToStart => 'Pošlite správu na začiatok';
|
||||||
|
|
||||||
@@ -917,11 +1253,6 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_location => 'Lokalita';
|
String get chat_location => 'Lokalita';
|
||||||
|
|
||||||
@override
|
|
||||||
String chat_sendMessageTo(String contactName) {
|
|
||||||
return 'Pošli správu $contactName';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_typeMessage => 'Napište správu...';
|
String get chat_typeMessage => 'Napište správu...';
|
||||||
|
|
||||||
@@ -1074,6 +1405,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_pathManagement => 'Správa ciest';
|
String get chat_pathManagement => 'Správa ciest';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_ShowAllPaths => 'Zobraziť všetky cesty';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_routingMode => 'Režim trasy';
|
String get chat_routingMode => 'Režim trasy';
|
||||||
|
|
||||||
@@ -1233,6 +1567,12 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_title => 'Mapa uzlov';
|
String get map_title => 'Mapa uzlov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_lineOfSight => 'Line of Sight';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_losScreenTitle => 'Line of Sight';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_noNodesWithLocation => 'Žiadne uzly s údajmi o polohe';
|
String get map_noNodesWithLocation => 'Žiadne uzly s údajmi o polohe';
|
||||||
|
|
||||||
@@ -1290,6 +1630,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';
|
||||||
|
|
||||||
@@ -1335,6 +1678,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';
|
||||||
|
|
||||||
@@ -1350,6 +1696,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';
|
||||||
|
|
||||||
@@ -1368,6 +1724,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ý';
|
||||||
|
|
||||||
@@ -1669,10 +2028,10 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Pošlite príkazy opakovaču';
|
String get repeater_cliSubtitle => 'Pošlite príkazy opakovaču';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Súsezný';
|
String get repeater_neighbors => 'Súsezný';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle => 'Zobraziť susedné body bez skokov.';
|
String get repeater_neighborsSubtitle => 'Zobraziť susedné body bez skokov.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => 'Nastavenia';
|
String get repeater_settings => 'Nastavenia';
|
||||||
@@ -2050,6 +2409,12 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get repeater_cliQuickClock => 'Hodiny';
|
String get repeater_cliQuickClock => 'Hodiny';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get repeater_cliQuickClockSync => 'Synchronizácia hodin';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get repeater_cliQuickDiscovery => 'Objaviť susedov';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_cliHelpAdvert => 'Odosiela reklamnú balíček.';
|
String get repeater_cliHelpAdvert => 'Odosiela reklamnú balíček.';
|
||||||
|
|
||||||
@@ -2363,7 +2728,7 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Opakovadlá Súsezná';
|
String get neighbors_repeatersNeighbors => 'Opakovadlá Súsezná';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData =>
|
String get neighbors_noData =>
|
||||||
@@ -2672,6 +3037,15 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get listFilter_all => 'Všetko';
|
String get listFilter_all => 'Všetko';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_favorites => 'Obľúbené';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_addToFavorites => 'Pridaj do obľúbených';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_removeFromFavorites => 'Odstrániť z označení';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_users => 'Používatelia';
|
String get listFilter_users => 'Používatelia';
|
||||||
|
|
||||||
@@ -2706,6 +3080,144 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get pathTrace_clearTooltip => 'Zmazať cestu';
|
String get pathTrace_clearTooltip => 'Zmazať cestu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectStartEnd => 'Vyberte počiatočný a koncový uzol pre LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losRunFailed(String error) {
|
||||||
|
return 'Kontrola priamej viditeľnosti zlyhala: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losClearAllPoints => 'Vymazať všetky body';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRunToViewElevationProfile =>
|
||||||
|
'Ak chcete zobraziť výškový profil, spustite LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuTitle => 'Menu LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuSubtitle =>
|
||||||
|
'Klepnutím na uzly alebo dlhým stlačením mapy získate vlastné body';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowDisplayNodes => 'Zobraziť uzly zobrazenia';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losCustomPoints => 'Vlastné body';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losCustomPointLabel(int index) {
|
||||||
|
return 'Vlastné $index';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointA => 'Bod A';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointB => 'Bod B';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaA(String value, String unit) {
|
||||||
|
return 'Anténa A: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaB(String value, String unit) {
|
||||||
|
return 'Anténa B: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRun => 'Spustite LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losNoElevationData => 'Žiadne údaje o nadmorskej výške';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileClear(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String clearance,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, vymazať LOS, min. vôľa $clearance $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileBlocked(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, blokovaný $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusChecking => 'LOS: kontrolujem...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusNoData => 'LOS: žiadne údaje';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||||
|
return 'LOS: $clear/$total vymazané, $blocked blokované, $unknown neznáme';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorElevationUnavailable =>
|
||||||
|
'Údaje o nadmorskej výške nie sú k dispozícii pre jednu alebo viacero vzoriek.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorInvalidInput =>
|
||||||
|
'Neplatné body/údaje o nadmorskej výške pre výpočet LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRenameCustomPoint => 'Premenovať vlastný bod';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointName => 'Názov bodu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowPanelTooltip => 'Zobraziť panel LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losHidePanelTooltip => 'Skryť panel LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losElevationAttribution =>
|
||||||
|
'Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendRadioHorizon => 'Rádiový horizont';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendLosBeam => 'Priama viditeľnosť';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendTerrain => 'Terén';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyLabel => 'Frekvencia';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyInfoTooltip => 'Zobraziť podrobnosti výpočtu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyDialogTitle => 'Výpočet rádiového horizontu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losFrequencyDialogDescription(
|
||||||
|
double baselineK,
|
||||||
|
double baselineFreq,
|
||||||
|
double frequencyMHz,
|
||||||
|
double kFactor,
|
||||||
|
) {
|
||||||
|
return 'Počnúc od k=$baselineK pri $baselineFreq MHz výpočet upraví k-faktor pre aktuálne pásmo $frequencyMHz MHz, ktorý definuje zakrivený strop rádiového horizontu.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_pathTrace => 'Sledovanie lúčov';
|
String get contacts_pathTrace => 'Sledovanie lúčov';
|
||||||
|
|
||||||
@@ -2877,4 +3389,278 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open export dát GPX mapových údajov';
|
'meshcore-open export dát GPX mapových údajov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Miestne opakovače';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Naposledy videný';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_title => 'Nastavenia kontaktov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddTitle => 'Automatické zisťovanie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_otherTitle =>
|
||||||
|
'Ďalšie nastavenia súvisiace s kontaktami';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersTitle =>
|
||||||
|
'Automaticky pridávať užívateľov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersSubtitle =>
|
||||||
|
'Povoliť spoločníkovi automaticky pridávať objavených užívateľov.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersTitle =>
|
||||||
|
'Automaticky pridávať opakovače';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||||
|
'Povoliť spoločníkovi automaticky pridávať objavené repeater.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersTitle =>
|
||||||
|
'Automaticky pridávať server miestnosti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||||
|
'Povoliť spoločníkovi automaticky pridať objavené serverové miestnosti.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsTitle =>
|
||||||
|
'Automaticky pridávať senzory';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||||
|
'Povoliť spoločníkovi automaticky pridávať objavené senzory.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestTitle => 'Prepísať najstaršie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestSubtitle =>
|
||||||
|
'Keď je zoznam kontaktov plný, bude nahradený najstarší neoznačený kontakt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_Title => 'Objavené kontakty';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_noMatching => 'Žiadne zhodné kontakty';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_searchHint => 'Vyhľadať objavené kontakty';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_contactAdded => 'Kontakt bol pridaný';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_addContact => 'Pridať kontakt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_copyContact => 'Kopírovať kontakt do schránky';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContact => 'Zmazať kontakt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAll =>
|
||||||
|
'Zmazať všetky objavené kontakty';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAllContent =>
|
||||||
|
'Ste si istí, že chcete zmazať všetky objavené kontakty?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_sendCooldown => 'Prosím, počkajte chvíľu, než zašlete znova.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_jumpToOldestUnread => 'Presk oceň';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_jumpToOldestUnreadSubtitle =>
|
||||||
|
'Pri otvorení chatu s neprečítanými správami, prejdite do prvého neprečítaného, namiesto poslednej.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageHu => 'Maďarský';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageJa => 'Japonský';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageKo => 'Kórejský';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_tooltip => 'Statistiky rádiových a sieťových kanálov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_screenTitle => 'Štatistiky rádiových vysielaní';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_notConnected =>
|
||||||
|
'Pripojte sa k zariadeniu, aby ste mohli sledovať štatistiky rádiového vysielania.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_firmwareTooOld =>
|
||||||
|
'Statistické údaje z rádia vyžadujú sprievodný softvér verzie v8 alebo novšej.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_waiting => 'Čakám na údaje…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_noiseFloor(int noiseDbm) {
|
||||||
|
return 'Úroveň hluku: $noiseDbm dBm';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_lastRssi(int rssiDbm) {
|
||||||
|
return 'Posledný údaj RSSI: $rssiDbm dBm';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_lastSnr(String snr) {
|
||||||
|
return 'Posledná hodnota SNR: $snr dB';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_txAir(int seconds) {
|
||||||
|
return 'Čas vysielania na TX (celkový): $seconds s';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_rxAir(int seconds) {
|
||||||
|
return 'Čas RX (celkový): $seconds s';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_chartCaption =>
|
||||||
|
'Úroveň šumu (dBm) pre posledné vzorky.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_stripNoise(int noiseDbm) {
|
||||||
|
return 'Úroveň hluku: $noiseDbm dBm';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_stripWaiting => 'Získavanie údajov o rádiu…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_settingsTile => 'Štatistiky rádiových vysielaní';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_settingsSubtitle =>
|
||||||
|
'Úroveň hluku, RSSI, SNR a časové rozloženie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_title => 'Preklad';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_enableTitle => 'Aktivovať preklad';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_enableSubtitle =>
|
||||||
|
'Prekladajte prichádzajúce správy a umožnite ich preklad pred odoslaním.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerTitle => 'Preložte pred odeslaním';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerSubtitle =>
|
||||||
|
'Riadi výchoce stav ikony pre preklad, ktorú používa program.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_targetLanguage => 'Cieľový jazyk';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_useAppLanguage => 'Použite jazyk aplikácie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadedModelLabel => 'Stiahnutý model';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_presetModelLabel =>
|
||||||
|
'Prednastavený model od Hugging Face';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_manualUrlLabel =>
|
||||||
|
'Odkaz na manuál (v elektronickej forme)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadModel => 'Stiahnuť model';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloading => 'Stiahnutie...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_working => 'Práca...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_stop => 'Zastavte';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_mergingChunks =>
|
||||||
|
'Sliečenie stiahnutých častí do konečného súboru...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadedModels => 'Stiahnuté modely';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_deleteModel => 'Odstrániť model';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_modelDownloaded => 'Model pre preklad bol stiahnutý.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadStopped => 'Stiahnutie bolo prerušené.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String translation_downloadFailed(String error) {
|
||||||
|
return 'Neúspešné stiahnutie: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_enterUrlFirst =>
|
||||||
|
'Najprv zadajte URL pre konkrétny model.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_linuxPairingShowPin => 'Zobraziť PIN';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_linuxPairingHidePin => 'Skryť PIN';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_linuxPairingPinTitle => 'PIN pre párovanie cez Bluetooth';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||||
|
return 'Zadajte PIN pre $deviceName (ak neexistuje, nechajte prázdne).';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_messageTranslation => 'Preklad textu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_translateBeforeSending => 'Preložte pred odeslaním';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerEnabledHint =>
|
||||||
|
'Správy budú preložené, než budú odoslané.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerDisabledHint =>
|
||||||
|
'Posielajte správy v pôvodnej písanom jazyku.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String translation_translateTo(String language) {
|
||||||
|
return 'Preložte do $language';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_translationOptions => 'Možnosti prekladania';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_systemLanguage => 'Jazyk systému';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
@@ -319,6 +507,10 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
String get settings_aboutDescription =>
|
String get settings_aboutDescription =>
|
||||||
'Odprtokodni Flutter klient za naprave za LoRa omrežje MeshCore.';
|
'Odprtokodni Flutter klient za naprave za LoRa omrežje MeshCore.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_aboutOpenMeteoAttribution =>
|
||||||
|
'Podatki o višini LOS: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_infoName => 'Ime';
|
String get settings_infoName => 'Ime';
|
||||||
|
|
||||||
@@ -455,6 +647,13 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageUk => 'Ukrajinsko';
|
String get appSettings_languageUk => 'Ukrajinsko';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracing => 'Omogoči sledenje sporočilom';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracingSubtitle =>
|
||||||
|
'Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Obvestila';
|
String get appSettings_notifications => 'Obvestila';
|
||||||
|
|
||||||
@@ -532,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';
|
||||||
|
|
||||||
@@ -615,6 +857,15 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_offlineMapCache => 'Shramba zemljevidov brez povezave';
|
String get appSettings_offlineMapCache => 'Shramba zemljevidov brez povezave';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsTitle => 'Enote';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsMetric => 'Metrična (m/km)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsImperial => 'Imperialno (ft / mi)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_noAreaSelected => 'Območje ni izbrano';
|
String get appSettings_noAreaSelected => 'Območje ni izbrano';
|
||||||
|
|
||||||
@@ -652,7 +903,35 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
'Stiki se bodo prikazali, ko se naprave oglasijo.';
|
'Stiki se bodo prikazali, ko se naprave oglasijo.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_searchContacts => 'Iskanje stikov...';
|
String get contacts_unread => 'Neprebrano';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_searchContactsNoNumber => 'Iskanje stikov...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchContacts(int number, String str) {
|
||||||
|
return 'Iskanje stikov...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchFavorites(int number, String str) {
|
||||||
|
return 'Iskanje $number$str priljubljenih...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchUsers(int number, String str) {
|
||||||
|
return 'Išči $number$str uporabnikov...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRepeaters(int number, String str) {
|
||||||
|
return 'Išči $number$str ponavljalnike...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRoomServers(int number, String str) {
|
||||||
|
return 'Išči $number$str strežnikov sob...';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_noUnreadContacts => 'Ne prebrani stiki.';
|
String get contacts_noUnreadContacts => 'Ne prebrani stiki.';
|
||||||
@@ -700,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';
|
||||||
@@ -739,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';
|
||||||
|
|
||||||
@@ -777,6 +1094,12 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_editChannel => 'Uredi kanal';
|
String get channels_editChannel => 'Uredi kanal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_muteChannel => 'Utišaj kanal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_unmuteChannel => 'Vklopi obvestila kanala';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_deleteChannel => 'Pošlji kanal';
|
String get channels_deleteChannel => 'Pošlji kanal';
|
||||||
|
|
||||||
@@ -785,6 +1108,11 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
return 'Izbrišem \"$name\"? To se ne da povrniti.';
|
return 'Izbrišem \"$name\"? To se ne da povrniti.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channels_channelDeleteFailed(String name) {
|
||||||
|
return 'Kanala $name ni bilo mogoče izbrisati';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String channels_channelDeleted(String name) {
|
String channels_channelDeleted(String name) {
|
||||||
return 'Kanal \"$name\" izbrisan.';
|
return 'Kanal \"$name\" izbrisan.';
|
||||||
@@ -896,6 +1224,14 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_noMessages => 'Še ni sporočil.';
|
String get chat_noMessages => 'Še ni sporočil.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_sendMessage => 'Send message';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String chat_sendMessageTo(String contactName) {
|
||||||
|
return 'Pošlji sporočilo $contactName';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_sendMessageToStart => 'Pošlji sporočilo za začetek.';
|
String get chat_sendMessageToStart => 'Pošlji sporočilo za začetek.';
|
||||||
|
|
||||||
@@ -916,11 +1252,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_location => 'Lokacija';
|
String get chat_location => 'Lokacija';
|
||||||
|
|
||||||
@override
|
|
||||||
String chat_sendMessageTo(String contactName) {
|
|
||||||
return 'Pošlji sporočilo $contactName';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_typeMessage => 'Vnesi sporočilo...';
|
String get chat_typeMessage => 'Vnesi sporočilo...';
|
||||||
|
|
||||||
@@ -1072,6 +1403,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_pathManagement => 'Upravljanje poti';
|
String get chat_pathManagement => 'Upravljanje poti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_ShowAllPaths => 'Prikaži vse poti';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_routingMode => 'Navodilo za usmerjevalni način';
|
String get chat_routingMode => 'Navodilo za usmerjevalni način';
|
||||||
|
|
||||||
@@ -1228,6 +1562,12 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_title => 'Mapa omrežja';
|
String get map_title => 'Mapa omrežja';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_lineOfSight => 'Linija vida';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_losScreenTitle => 'Linija vida';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_noNodesWithLocation =>
|
String get map_noNodesWithLocation =>
|
||||||
'Nihče od notranjih elementov nima podatkov o lokaciji.';
|
'Nihče od notranjih elementov nima podatkov o lokaciji.';
|
||||||
@@ -1286,6 +1626,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';
|
||||||
|
|
||||||
@@ -1331,6 +1674,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';
|
||||||
|
|
||||||
@@ -1346,6 +1692,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';
|
||||||
|
|
||||||
@@ -1364,6 +1719,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';
|
||||||
|
|
||||||
@@ -1668,10 +2026,10 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
'Pošlji ukazne povelje na ponovitveno enoto.';
|
'Pošlji ukazne povelje na ponovitveno enoto.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Sosedi';
|
String get repeater_neighbors => 'Sosedi';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle => 'Pogledati nič sosednjih hopjev.';
|
String get repeater_neighborsSubtitle => 'Pogledati nič sosednjih hopjev.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => 'Nastavitve';
|
String get repeater_settings => 'Nastavitve';
|
||||||
@@ -2054,6 +2412,12 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get repeater_cliQuickClock => 'Ura';
|
String get repeater_cliQuickClock => 'Ura';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get repeater_cliQuickClockSync => 'Usklajevanje ure';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get repeater_cliQuickDiscovery => 'Odkrijte sosede';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_cliHelpAdvert => 'Pošlje paket oglasov';
|
String get repeater_cliHelpAdvert => 'Pošlje paket oglasov';
|
||||||
|
|
||||||
@@ -2367,7 +2731,7 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Ponovitve Sosedi';
|
String get neighbors_repeatersNeighbors => 'Ponovitve Sosedi';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Niso na voljo podatki o sosedih.';
|
String get neighbors_noData => 'Niso na voljo podatki o sosedih.';
|
||||||
@@ -2675,6 +3039,15 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get listFilter_all => 'Vse';
|
String get listFilter_all => 'Vse';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_favorites => 'Priljubljene';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_addToFavorites => 'Dodaj v priljubljene';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_removeFromFavorites => 'Odstrani iz priljubljenih';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_users => 'Uporabniki';
|
String get listFilter_users => 'Uporabniki';
|
||||||
|
|
||||||
@@ -2709,6 +3082,144 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get pathTrace_clearTooltip => 'Počisti pot';
|
String get pathTrace_clearTooltip => 'Počisti pot';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectStartEnd => 'Izberite začetno in končno vozlišče za LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losRunFailed(String error) {
|
||||||
|
return 'Preverjanje vidnega polja ni uspelo: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losClearAllPoints => 'Počisti vse točke';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRunToViewElevationProfile =>
|
||||||
|
'Zaženite LOS za ogled višinskega profila';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuTitle => 'LOS meni';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuSubtitle =>
|
||||||
|
'Tapnite vozlišča ali dolgo pritisnite na zemljevid za točke po meri';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowDisplayNodes => 'Pokaži prikazna vozlišča';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losCustomPoints => 'Točke po meri';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losCustomPointLabel(int index) {
|
||||||
|
return 'Po meri $index';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointA => 'Točka A';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointB => 'Točka B';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaA(String value, String unit) {
|
||||||
|
return 'Antena A: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaB(String value, String unit) {
|
||||||
|
return 'Antena B: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRun => 'Zaženi LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losNoElevationData => 'Ni podatkov o višini';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileClear(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String clearance,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, čisti LOS, najmanjša razdalja $clearance $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileBlocked(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, blokiral $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusChecking => 'LOS: preverjam ...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusNoData => 'LOS: ni podatkov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||||
|
return 'LOS: $clear/$total jasno, $blocked blokirano, $unknown neznano';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorElevationUnavailable =>
|
||||||
|
'Podatki o nadmorski višini niso na voljo za enega ali več vzorcev.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorInvalidInput =>
|
||||||
|
'Neveljavni podatki o točkah/višini za izračun LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRenameCustomPoint => 'Preimenujte točko po meri';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointName => 'Ime točke';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowPanelTooltip => 'Pokaži ploščo LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losHidePanelTooltip => 'Skrij ploščo LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losElevationAttribution =>
|
||||||
|
'Podatki o višini: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendRadioHorizon => 'Radijski horizont';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendLosBeam => 'Linija vidnosti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendTerrain => 'Teren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyLabel => 'Frekvenca';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyInfoTooltip => 'Prikaži podrobnosti izračuna';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyDialogTitle => 'Izračun radijskega horizonta';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losFrequencyDialogDescription(
|
||||||
|
double baselineK,
|
||||||
|
double baselineFreq,
|
||||||
|
double frequencyMHz,
|
||||||
|
double kFactor,
|
||||||
|
) {
|
||||||
|
return 'Začenši od k=$baselineK pri $baselineFreq MHz, izračun prilagodi k-faktor za trenutni pas $frequencyMHz MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_pathTrace => 'Sledenje poti';
|
String get contacts_pathTrace => 'Sledenje poti';
|
||||||
|
|
||||||
@@ -2882,4 +3393,279 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open izvoz podatkov GPX karte';
|
'meshcore-open izvoz podatkov GPX karte';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Bližnji ponovitelji';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Zadnjič videno';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_title => 'Nastavitve stikov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddTitle => 'Avtomatsko odkrivanje';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_otherTitle => 'Druge nastavitve v zvezi s stiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersTitle =>
|
||||||
|
'Avtomatsko dodaj uporabnike';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersSubtitle =>
|
||||||
|
'Dovoli spremljevalcu, da samodejno doda odkrite uporabnike.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersTitle =>
|
||||||
|
'Avtomatsko dodaj ponovitelje';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||||
|
'Dovoli spremljevalcu, da samodejno doda odkrite ponovitelje.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersTitle =>
|
||||||
|
'Avtomatsko dodaj strežnike sob';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||||
|
'Dovoli spremljevalcu, da samodejno doda odkrite strežnike sob.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsTitle =>
|
||||||
|
'Avtomatsko dodaj senzorje';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||||
|
'Dovoli spremljevalcu, da samodejno doda odkrite senzorje.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestTitle => 'Prepiši najstarejše';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestSubtitle =>
|
||||||
|
'Ko je seznam stikov poln, bo najstarejši nestarševski stik zamenjan.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_Title => 'Odkriti stiki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_noMatching => 'Ni ujemajočih stikov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_searchHint => 'Najdeni stiki po iskanju';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_contactAdded => 'Kontakt dodan';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_addContact => 'Dodaj stik';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_copyContact => 'Kopiraj stik v odložišče';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContact => 'Izbriši stik';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAll =>
|
||||||
|
'Izbriši vse odkrite kontakte';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAllContent =>
|
||||||
|
'Ste prepričani, da želite izbrisati vse odkrite kontakte?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_sendCooldown =>
|
||||||
|
'Prosimo, počakajte trenutek, preden pošljete ponovno.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_jumpToOldestUnread =>
|
||||||
|
'Pritisnite za najstarejše nepročitano sporočilo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_jumpToOldestUnreadSubtitle =>
|
||||||
|
'Ko odpirate klepet z neprebranimi sporočili, se premaknite na prvo neprebrano sporočilo, namesto najnovejšega.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageHu => 'Madžarski';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageJa => 'Japonski';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageKo => 'Korejski';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_tooltip => 'Statistike za radio in mrežo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_screenTitle => 'Radijske statistike';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_notConnected =>
|
||||||
|
'Povežite se z napravo, da si ogledate statistiko o radiju.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_firmwareTooOld =>
|
||||||
|
'Statistika za radio zahteva združljivo programsko opremo v8 ali kasnejše.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_waiting => 'Čakam na podatke…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_noiseFloor(int noiseDbm) {
|
||||||
|
return 'Število šuma: $noiseDbm dBm';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_lastRssi(int rssiDbm) {
|
||||||
|
return 'Najkasnejše vrednost RSSI: $rssiDbm dBm';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_lastSnr(String snr) {
|
||||||
|
return 'Najkasnejše vrednost SNR: $snr dB';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_txAir(int seconds) {
|
||||||
|
return 'Čas na TX (skupno): $seconds s';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_rxAir(int seconds) {
|
||||||
|
return 'Čas, namenjen RX-ju (skupno): $seconds s';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_chartCaption =>
|
||||||
|
'Ravnovredna raven šuma (dBm) za nedavne vzorce.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_stripNoise(int noiseDbm) {
|
||||||
|
return 'Število šuma: $noiseDbm dBm';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_stripWaiting => 'Prejemanje statistike o radiju…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_settingsTile => 'Radijske statistike';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_settingsSubtitle =>
|
||||||
|
'Število šumov, RSSI, SNR in čas, ki ga je napolnila oprema';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_title => 'Prevod';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_enableTitle => 'Omogočite prevod';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_enableSubtitle =>
|
||||||
|
'Prevedite vstopne sporočila in omogočite predhodno prevajanje.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerTitle => 'Preprištejte, preden pošljete';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerSubtitle =>
|
||||||
|
'Ureja privzeto stanje ikone za prevod, ki jo uporablja avtor.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_targetLanguage => 'Ciljna jezika';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_useAppLanguage => 'Uporabite jezik aplikacije';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadedModelLabel => 'Naložen model';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_presetModelLabel =>
|
||||||
|
'Prednastavljeni model Hugging Face';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_manualUrlLabel => 'URL za ročni model';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadModel => 'Prenesite model';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloading => 'Izvajanje...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_working => 'Delo...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_stop => 'Prekliji';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_mergingChunks =>
|
||||||
|
'Sklapljanje prenesenih delov v končni datoteko...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadedModels => 'Naloženi modeli';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_deleteModel => 'Izbrisati model';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_modelDownloaded =>
|
||||||
|
'Model za prevajanje je bil naložen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadStopped => 'Prenos je bil prekinjen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String translation_downloadFailed(String error) {
|
||||||
|
return 'Izgovoritev ni bila uspešna: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_enterUrlFirst => 'Najprej vnesite URL model.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_linuxPairingShowPin => 'Prikaži PIN';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_linuxPairingHidePin => 'Skrij PIN';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_linuxPairingPinTitle => 'Bluetooth PIN za seznanjanje';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||||
|
return 'Vnesite PIN za $deviceName (pustite prazno, če ga ni).';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_messageTranslation => 'Prevod sporočila';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_translateBeforeSending =>
|
||||||
|
'Preprištejte, preden pošljete';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerEnabledHint =>
|
||||||
|
'Vsebina sporočil bo prevedena, preden jih pošljemo.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerDisabledHint =>
|
||||||
|
'Pošljite sporočila v originalnem tipkanem jeziku.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String translation_translateTo(String language) {
|
||||||
|
return 'Prevesti v $language';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_translationOptions => 'Možnosti prevoda';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_systemLanguage => 'Jezik sistema';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
@@ -317,6 +504,10 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
String get settings_aboutDescription =>
|
String get settings_aboutDescription =>
|
||||||
'En öppen källkods Flutter-klient för MeshCore LoRa meshnätverksenheter.';
|
'En öppen källkods Flutter-klient för MeshCore LoRa meshnätverksenheter.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_aboutOpenMeteoAttribution =>
|
||||||
|
'LOS-höjddata: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_infoName => 'Namn';
|
String get settings_infoName => 'Namn';
|
||||||
|
|
||||||
@@ -453,6 +644,13 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageUk => 'Ukrainska';
|
String get appSettings_languageUk => 'Ukrainska';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracing => 'Aktivera meddelandespårning';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracingSubtitle =>
|
||||||
|
'Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Meddelanden';
|
String get appSettings_notifications => 'Meddelanden';
|
||||||
|
|
||||||
@@ -527,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';
|
||||||
|
|
||||||
@@ -610,6 +850,15 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_offlineMapCache => 'Offline Kartcache';
|
String get appSettings_offlineMapCache => 'Offline Kartcache';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsTitle => 'Enheter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsMetric => 'Metriskt (m/km)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsImperial => 'Imperialt (ft / mi)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_noAreaSelected => 'Ingen area markerad';
|
String get appSettings_noAreaSelected => 'Ingen area markerad';
|
||||||
|
|
||||||
@@ -647,7 +896,35 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
'Kontakter kommer att visas när enheter annonserar.';
|
'Kontakter kommer att visas när enheter annonserar.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_searchContacts => 'Sök kontakter...';
|
String get contacts_unread => 'Oläst';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_searchContactsNoNumber => 'Sök kontakter...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchContacts(int number, String str) {
|
||||||
|
return 'Sök kontakter...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchFavorites(int number, String str) {
|
||||||
|
return 'Sök $number$str Favoriter...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchUsers(int number, String str) {
|
||||||
|
return 'Sök $number$str användare...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRepeaters(int number, String str) {
|
||||||
|
return 'Sök $number$str upprepningsenheter...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRoomServers(int number, String str) {
|
||||||
|
return 'Sök $number$str Room-servrar...';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_noUnreadContacts => 'Inga oinlästa kontakter';
|
String get contacts_noUnreadContacts => 'Inga oinlästa kontakter';
|
||||||
@@ -696,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.';
|
||||||
@@ -735,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';
|
||||||
|
|
||||||
@@ -773,6 +1087,12 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_editChannel => 'Redigera kanal';
|
String get channels_editChannel => 'Redigera kanal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_muteChannel => 'Tysta kanal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_unmuteChannel => 'Slå på ljud för kanal';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_deleteChannel => 'Ta bort kanal';
|
String get channels_deleteChannel => 'Ta bort kanal';
|
||||||
|
|
||||||
@@ -781,6 +1101,11 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
return 'Radera \"$name\"? Detta kan inte ångras.';
|
return 'Radera \"$name\"? Detta kan inte ångras.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channels_channelDeleteFailed(String name) {
|
||||||
|
return 'Det gick inte att ta bort kanalen \"$name\"';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String channels_channelDeleted(String name) {
|
String channels_channelDeleted(String name) {
|
||||||
return 'Kanalen \"$name\" raderad';
|
return 'Kanalen \"$name\" raderad';
|
||||||
@@ -892,6 +1217,14 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_noMessages => 'Inga meddelanden ännu';
|
String get chat_noMessages => 'Inga meddelanden ännu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_sendMessage => 'Send message';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String chat_sendMessageTo(String contactName) {
|
||||||
|
return 'Skicka ett meddelande till $contactName';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_sendMessageToStart =>
|
String get chat_sendMessageToStart =>
|
||||||
'Skicka ett meddelande för att komma igång';
|
'Skicka ett meddelande för att komma igång';
|
||||||
@@ -913,11 +1246,6 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_location => 'Plats';
|
String get chat_location => 'Plats';
|
||||||
|
|
||||||
@override
|
|
||||||
String chat_sendMessageTo(String contactName) {
|
|
||||||
return 'Skicka ett meddelande till $contactName';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_typeMessage => 'Skriv ett meddelande...';
|
String get chat_typeMessage => 'Skriv ett meddelande...';
|
||||||
|
|
||||||
@@ -1069,6 +1397,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_pathManagement => 'Stigarhantering';
|
String get chat_pathManagement => 'Stigarhantering';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_ShowAllPaths => 'Visa alla vägar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_routingMode => 'Ruttläge';
|
String get chat_routingMode => 'Ruttläge';
|
||||||
|
|
||||||
@@ -1225,6 +1556,12 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_title => 'Nodkarta';
|
String get map_title => 'Nodkarta';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_lineOfSight => 'Synlinje';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_losScreenTitle => 'Synlinje';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_noNodesWithLocation => 'Inga noder med platsinformation';
|
String get map_noNodesWithLocation => 'Inga noder med platsinformation';
|
||||||
|
|
||||||
@@ -1282,6 +1619,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';
|
||||||
|
|
||||||
@@ -1327,6 +1667,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';
|
||||||
|
|
||||||
@@ -1342,6 +1685,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';
|
||||||
|
|
||||||
@@ -1360,6 +1713,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';
|
||||||
|
|
||||||
@@ -1658,10 +2014,10 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Skicka kommandon till repetitorn';
|
String get repeater_cliSubtitle => 'Skicka kommandon till repetitorn';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Grannar';
|
String get repeater_neighbors => 'Grannar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle => 'Visa noll hoppgrannar.';
|
String get repeater_neighborsSubtitle => 'Visa noll hoppgrannar.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_settings => 'Inställningar';
|
String get repeater_settings => 'Inställningar';
|
||||||
@@ -2041,6 +2397,12 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get repeater_cliQuickClock => 'Klocka';
|
String get repeater_cliQuickClock => 'Klocka';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get repeater_cliQuickClockSync => 'Synkronisera klocka';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get repeater_cliQuickDiscovery => 'Upptäck grannar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_cliHelpAdvert => 'Skickar ett annonspaket';
|
String get repeater_cliHelpAdvert => 'Skickar ett annonspaket';
|
||||||
|
|
||||||
@@ -2352,7 +2714,7 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Upprepar grannar';
|
String get neighbors_repeatersNeighbors => 'Upprepar grannar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Inga grannuppgifter finns tillgängliga.';
|
String get neighbors_noData => 'Inga grannuppgifter finns tillgängliga.';
|
||||||
@@ -2660,6 +3022,15 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get listFilter_all => 'Alla';
|
String get listFilter_all => 'Alla';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_favorites => 'Favoriter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_addToFavorites => 'Lägg till i favoriter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_removeFromFavorites => 'Ta bort från favoriter';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_users => 'Användare';
|
String get listFilter_users => 'Användare';
|
||||||
|
|
||||||
@@ -2694,6 +3065,142 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get pathTrace_clearTooltip => 'Rensa väg';
|
String get pathTrace_clearTooltip => 'Rensa väg';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectStartEnd => 'Välj start- och slutnoder för LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losRunFailed(String error) {
|
||||||
|
return 'Synlinjekontroll misslyckades: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losClearAllPoints => 'Rensa alla punkter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRunToViewElevationProfile => 'Kör LOS för att se höjdprofil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuTitle => 'LOS-menyn';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuSubtitle =>
|
||||||
|
'Tryck på noder eller tryck länge på kartan för anpassade punkter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowDisplayNodes => 'Visa displaynoder';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losCustomPoints => 'Anpassade poäng';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losCustomPointLabel(int index) {
|
||||||
|
return 'Anpassad $index';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointA => 'Punkt A';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointB => 'Punkt B';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaA(String value, String unit) {
|
||||||
|
return 'Antenn A: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaB(String value, String unit) {
|
||||||
|
return 'Antenn B: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRun => 'Kör LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losNoElevationData => 'Inga höjddata';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileClear(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String clearance,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, rensa LOS, min clearance $clearance $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileBlocked(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, blockerad av $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusChecking => 'LOS: kollar...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusNoData => 'LOS: inga data';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||||
|
return 'LOS: $clear/$total rensa, $blocked blockerad, $unknown okänd';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorElevationUnavailable =>
|
||||||
|
'Höjddata är inte tillgänglig för ett eller flera prover.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorInvalidInput =>
|
||||||
|
'Ogiltiga poäng/höjddata för LOS-beräkning.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRenameCustomPoint => 'Byt namn på anpassad punkt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointName => 'Punktnamn';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowPanelTooltip => 'Visa LOS-panelen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losHidePanelTooltip => 'Dölj LOS-panelen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losElevationAttribution => 'Höjddata: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendRadioHorizon => 'Radiohorisont';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendLosBeam => 'Siktlinje';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendTerrain => 'Terräng';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyLabel => 'Frekvens';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyInfoTooltip => 'Visa detaljer om beräkningen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyDialogTitle => 'Beräkning av radiohorisonten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losFrequencyDialogDescription(
|
||||||
|
double baselineK,
|
||||||
|
double baselineFreq,
|
||||||
|
double frequencyMHz,
|
||||||
|
double kFactor,
|
||||||
|
) {
|
||||||
|
return 'Med start från k=$baselineK vid $baselineFreq MHz, justerar beräkningen k-faktorn för det aktuella $frequencyMHz MHz-bandet, som definierar den böjda radiohorisonten.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_pathTrace => 'Path Trace';
|
String get contacts_pathTrace => 'Path Trace';
|
||||||
|
|
||||||
@@ -2862,4 +3369,280 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'meshcore-open export av GPX-kartdata';
|
'meshcore-open export av GPX-kartdata';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Närliggande uppreparstationer';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Senast sedd';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_title => 'Kontaktinställningar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddTitle => 'Automatisk upptäckt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_otherTitle =>
|
||||||
|
'Andra inställningar relaterade till kontakt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersTitle =>
|
||||||
|
'Lägg till användare automatiskt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersSubtitle =>
|
||||||
|
'Tillåt kompanjonen att automatiskt lägga till upptäckta användare';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersTitle =>
|
||||||
|
'Lägg till upprepande enheter automatiskt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||||
|
'Tillåt kompanjonen att automatiskt lägga till upptäckta repeater.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersTitle =>
|
||||||
|
'Lägg automatiskt till rumsservrar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||||
|
'Tillåt kompanjonen att automatiskt lägga till upptäckta rumsservrar.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsTitle =>
|
||||||
|
'Lägg till sensorer automatiskt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||||
|
'Tillåt kompanjonen att automatiskt lägga till upptäckta sensorer.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestTitle => 'Skriv över äldst';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestSubtitle =>
|
||||||
|
'När kontaktlistan är full ersätts den äldsta icke-favoriterade kontakten.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_Title => 'Upptäckta kontakter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_noMatching => 'Inga matchande kontakter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_searchHint => 'Sök uppfunna kontakter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_contactAdded => 'Kontakt tillagd';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_addContact => 'Lägg till kontakt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_copyContact => 'Kopiera kontakt till urklipp';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContact => 'Ta bort kontakt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAll =>
|
||||||
|
'Ta bort alla upptäckta kontakter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAllContent =>
|
||||||
|
'Är du säker på att du vill ta bort alla upptäckta kontakter?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_sendCooldown =>
|
||||||
|
'Vänligen vänta en stund innan du skickar igen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_jumpToOldestUnread =>
|
||||||
|
'Gå direkt till det äldsta, obesvarade meddelandet';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_jumpToOldestUnreadSubtitle =>
|
||||||
|
'När du öppnar en chatt med oinlästa meddelanden, scrolla till det första oinlästa meddelandet istället för det senaste.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageHu => 'Ungerskt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageJa => 'Japanska';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageKo => 'Koreanska';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_tooltip => 'Radio- och mesh-statistik';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_screenTitle => 'Radiostation';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_notConnected =>
|
||||||
|
'Anslut till en enhet för att visa radiostatistik.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_firmwareTooOld =>
|
||||||
|
'Radio statistik kräver kompatibel firmware version 8 eller senare.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_waiting => 'Väntar på data…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_noiseFloor(int noiseDbm) {
|
||||||
|
return 'Bakgrundsnivå: $noiseDbm dBm';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_lastRssi(int rssiDbm) {
|
||||||
|
return 'Senaste RSSI-värde: $rssiDbm dBm';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_lastSnr(String snr) {
|
||||||
|
return 'Senaste SNR: $snr dB';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_txAir(int seconds) {
|
||||||
|
return 'TX-tid (total): $seconds sekunder';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_rxAir(int seconds) {
|
||||||
|
return 'RX-tid (total): $seconds s';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_chartCaption =>
|
||||||
|
'Ljudnivå (dBm) baserat på de senaste mätningarna.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_stripNoise(int noiseDbm) {
|
||||||
|
return 'Bakgrundsnivå: $noiseDbm dBm';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_stripWaiting => 'Hämtar radiostatistik…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_settingsTile => 'Radiostation';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_settingsSubtitle =>
|
||||||
|
'Bakgrundsnivå, RSSI, SNR och tillgänglig tid';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_title => 'Översättning';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_enableTitle => 'Aktivera översättning';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_enableSubtitle =>
|
||||||
|
'Översätt inkommande meddelanden och möjliggör översättning före avsändning.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerTitle => 'Översätt innan du skickar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerSubtitle =>
|
||||||
|
'Styr standardtillståndet för kompositorns översättningsikon.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_targetLanguage => 'Målmedvetet språk';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_useAppLanguage => 'Använd appens språk';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadedModelLabel => 'Nedladdad modell';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_presetModelLabel =>
|
||||||
|
'Fördefinierat Hugging Face-modell';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_manualUrlLabel => 'Manualens URL';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadModel => 'Ladda ner modellen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloading => 'Nedladdning...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_working => 'Arbeta...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_stop => 'Stopp';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_mergingChunks =>
|
||||||
|
'Slå samman de nedladdade delarna till en slutlig fil...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadedModels => 'Nedladdade modeller';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_deleteModel => 'Ta bort modell';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_modelDownloaded =>
|
||||||
|
'Översättningsmodellen har laddats ner.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadStopped => 'Nedladdningen avbruten.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String translation_downloadFailed(String error) {
|
||||||
|
return 'Nedladdning misslyckades: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_enterUrlFirst =>
|
||||||
|
'Ange först en URL för en specifik modell.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_linuxPairingShowPin => 'Visa PIN';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_linuxPairingHidePin => 'Dölj PIN';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_linuxPairingPinTitle => 'Bluetooth‑parnings‑PIN';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||||
|
return 'Ange PIN för $deviceName (lämna tomt om ingen).';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_messageTranslation => 'Meddelandets översättning';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_translateBeforeSending => 'Översätt innan du skickar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerEnabledHint =>
|
||||||
|
'Meddelandena kommer att översättas innan de skickas.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerDisabledHint =>
|
||||||
|
'Skicka meddelanden på det ursprungliga, stavade språket.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String translation_translateTo(String language) {
|
||||||
|
return 'Översätt till $language';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_translationOptions => 'Översättningsalternativ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_systemLanguage => 'Språk för systemet';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 => 'Дії';
|
||||||
|
|
||||||
@@ -322,6 +512,10 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
String get settings_aboutDescription =>
|
String get settings_aboutDescription =>
|
||||||
'Клієнт Flutter з відкритим вихідним кодом для пристроїв мережі MeshCore LoRa.';
|
'Клієнт Flutter з відкритим вихідним кодом для пристроїв мережі MeshCore LoRa.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settings_aboutOpenMeteoAttribution =>
|
||||||
|
'Дані про висоту LOS: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settings_infoName => 'Ім\'я';
|
String get settings_infoName => 'Ім\'я';
|
||||||
|
|
||||||
@@ -458,6 +652,14 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_languageUk => 'Українська';
|
String get appSettings_languageUk => 'Українська';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracing =>
|
||||||
|
'Увімкнути відстеження повідомлень';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_enableMessageTracingSubtitle =>
|
||||||
|
'Показувати детальні метадані про маршрутизацію та час для повідомлень';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_notifications => 'Сповіщення';
|
String get appSettings_notifications => 'Сповіщення';
|
||||||
|
|
||||||
@@ -534,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 => 'Батарея';
|
||||||
|
|
||||||
@@ -618,6 +863,15 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get appSettings_offlineMapCache => 'Офлайн-кеш карти';
|
String get appSettings_offlineMapCache => 'Офлайн-кеш карти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsTitle => 'одиниці';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsMetric => 'Метричний (м / км)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_unitsImperial => 'Імперська (ft / mi)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appSettings_noAreaSelected => 'Область не вибрано';
|
String get appSettings_noAreaSelected => 'Область не вибрано';
|
||||||
|
|
||||||
@@ -655,7 +909,35 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
'Контакти з\'являться, коли пристрої надішлють оголошення.';
|
'Контакти з\'являться, коли пристрої надішлють оголошення.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_searchContacts => 'Пошук контактів...';
|
String get contacts_unread => 'Непрочитане';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contacts_searchContactsNoNumber => 'Пошук контактів...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchContacts(int number, String str) {
|
||||||
|
return 'Пошук контактів...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchFavorites(int number, String str) {
|
||||||
|
return 'Пошук $number$str улюблених...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchUsers(int number, String str) {
|
||||||
|
return 'Пошук $number$str користувачів...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRepeaters(int number, String str) {
|
||||||
|
return 'Пошук $number$str ретрансляторів...';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contacts_searchRoomServers(int number, String str) {
|
||||||
|
return 'Пошук $number$str серверів кімнат...';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_noUnreadContacts => 'Немає непрочитаних контактів';
|
String get contacts_noUnreadContacts => 'Немає непрочитаних контактів';
|
||||||
@@ -703,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» вже існує.';
|
||||||
@@ -742,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 => 'Канали';
|
||||||
|
|
||||||
@@ -780,6 +1101,12 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels_editChannel => 'Редагувати канал';
|
String get channels_editChannel => 'Редагувати канал';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_muteChannel => 'Вимкнути сповіщення каналу';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channels_unmuteChannel => 'Увімкнути сповіщення каналу';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channels_deleteChannel => 'Видалити канал';
|
String get channels_deleteChannel => 'Видалити канал';
|
||||||
|
|
||||||
@@ -788,6 +1115,11 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
return 'Видалити $name? Це не можна скасувати.';
|
return 'Видалити $name? Це не можна скасувати.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channels_channelDeleteFailed(String name) {
|
||||||
|
return 'Не вдалося видалити канал \"$name\"';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String channels_channelDeleted(String name) {
|
String channels_channelDeleted(String name) {
|
||||||
return 'Канал «$name» видалено';
|
return 'Канал «$name» видалено';
|
||||||
@@ -898,6 +1230,14 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_noMessages => 'Поки немає повідомлень.';
|
String get chat_noMessages => 'Поки немає повідомлень.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_sendMessage => 'Send message';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String chat_sendMessageTo(String contactName) {
|
||||||
|
return 'Надіслати повідомлення $contactName';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_sendMessageToStart => 'Надішліть повідомлення, щоб почати';
|
String get chat_sendMessageToStart => 'Надішліть повідомлення, щоб почати';
|
||||||
|
|
||||||
@@ -918,11 +1258,6 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_location => 'Розташування';
|
String get chat_location => 'Розташування';
|
||||||
|
|
||||||
@override
|
|
||||||
String chat_sendMessageTo(String contactName) {
|
|
||||||
return 'Надіслати повідомлення $contactName';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_typeMessage => 'Введіть повідомлення...';
|
String get chat_typeMessage => 'Введіть повідомлення...';
|
||||||
|
|
||||||
@@ -1075,6 +1410,9 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chat_pathManagement => 'Керування шляхами';
|
String get chat_pathManagement => 'Керування шляхами';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_ShowAllPaths => 'Показати всі шляхи';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chat_routingMode => 'Режим маршрутизації';
|
String get chat_routingMode => 'Режим маршрутизації';
|
||||||
|
|
||||||
@@ -1237,6 +1575,12 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get map_title => 'Карта вузлів';
|
String get map_title => 'Карта вузлів';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_lineOfSight => 'Пряма видимість';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get map_losScreenTitle => 'Пряма видимість';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get map_noNodesWithLocation =>
|
String get map_noNodesWithLocation =>
|
||||||
'Немає вузлів з даними про розташування';
|
'Немає вузлів з даними про розташування';
|
||||||
@@ -1295,6 +1639,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 => 'Мітка піна';
|
||||||
|
|
||||||
@@ -1340,6 +1687,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 => 'Префікс ключа';
|
||||||
|
|
||||||
@@ -1355,6 +1705,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 => 'Час останньої активності';
|
||||||
|
|
||||||
@@ -1373,6 +1733,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 => 'Видалити останній';
|
||||||
|
|
||||||
@@ -1675,10 +2038,10 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
String get repeater_cliSubtitle => 'Надіслати команди ретранслятору';
|
String get repeater_cliSubtitle => 'Надіслати команди ретранслятору';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighbours => 'Сусіди';
|
String get repeater_neighbors => 'Сусіди';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_neighboursSubtitle =>
|
String get repeater_neighborsSubtitle =>
|
||||||
'Показати сусідів нульового стрибка.';
|
'Показати сусідів нульового стрибка.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -2067,6 +2430,12 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get repeater_cliQuickClock => 'Годинник';
|
String get repeater_cliQuickClock => 'Годинник';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get repeater_cliQuickClockSync => 'Синхронізація годинника';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get repeater_cliQuickDiscovery => 'Відкрити сусідів';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater_cliHelpAdvert => 'Надсилає пакет оголошення';
|
String get repeater_cliHelpAdvert => 'Надсилає пакет оголошення';
|
||||||
|
|
||||||
@@ -2380,7 +2749,7 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_repeatersNeighbours => 'Ретранслятори-сусіди';
|
String get neighbors_repeatersNeighbors => 'Ретранслятори-сусіди';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get neighbors_noData => 'Дані про сусідів недоступні.';
|
String get neighbors_noData => 'Дані про сусідів недоступні.';
|
||||||
@@ -2696,6 +3065,15 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get listFilter_all => 'Все';
|
String get listFilter_all => 'Все';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_favorites => 'Улюблені';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_addToFavorites => 'Додати до улюблених';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get listFilter_removeFromFavorites => 'Видалити зі списку улюблених';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get listFilter_users => 'Користувачі';
|
String get listFilter_users => 'Користувачі';
|
||||||
|
|
||||||
@@ -2730,6 +3108,145 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get pathTrace_clearTooltip => 'Очистити шлях';
|
String get pathTrace_clearTooltip => 'Очистити шлях';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losSelectStartEnd =>
|
||||||
|
'Виберіть початковий і кінцевий вузли для LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losRunFailed(String error) {
|
||||||
|
return 'Помилка перевірки прямої видимості: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losClearAllPoints => 'Очистити всі пункти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRunToViewElevationProfile =>
|
||||||
|
'Запустіть LOS, щоб переглянути профіль висоти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuTitle => 'Меню LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losMenuSubtitle =>
|
||||||
|
'Торкніться вузлів або утримуйте карту, щоб отримати власні точки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowDisplayNodes => 'Показати вузли відображення';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losCustomPoints => 'Користувальницькі точки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losCustomPointLabel(int index) {
|
||||||
|
return 'Спеціальний $index';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointA => 'Точка А';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointB => 'Точка Б';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaA(String value, String unit) {
|
||||||
|
return 'Антена A: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losAntennaB(String value, String unit) {
|
||||||
|
return 'Антена B: $value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRun => 'Запустіть LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losNoElevationData => 'Немає даних про висоту';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileClear(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String clearance,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, чистий LOS, мінімальний зазор $clearance $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losProfileBlocked(
|
||||||
|
String distance,
|
||||||
|
String distanceUnit,
|
||||||
|
String obstruction,
|
||||||
|
String heightUnit,
|
||||||
|
) {
|
||||||
|
return '$distance $distanceUnit, заблоковано $obstruction $heightUnit';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusChecking => 'LOS: перевірка...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losStatusNoData => 'LOS: немає даних';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||||
|
return 'LOS: $clear/$total очищено, $blocked заблоковано, $unknown невідомо';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorElevationUnavailable =>
|
||||||
|
'Дані про висоту недоступні для одного чи кількох зразків.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losErrorInvalidInput =>
|
||||||
|
'Недійсні дані про точки/висоту для розрахунку LOS.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losRenameCustomPoint => 'Перейменуйте спеціальну точку';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losPointName => 'Назва точки';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losShowPanelTooltip => 'Показати панель LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losHidePanelTooltip => 'Приховати панель LOS';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losElevationAttribution =>
|
||||||
|
'Дані про висоту: Open-Meteo (CC BY 4.0)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendRadioHorizon => 'Радіогоризонт';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendLosBeam => 'Лінія прямої видимості';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losLegendTerrain => 'Рельєф';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyLabel => 'Частота';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyInfoTooltip => 'Переглянути деталі розрахунку';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get losFrequencyDialogTitle => 'Розрахунок радіогоризонту';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String losFrequencyDialogDescription(
|
||||||
|
double baselineK,
|
||||||
|
double baselineFreq,
|
||||||
|
double frequencyMHz,
|
||||||
|
double kFactor,
|
||||||
|
) {
|
||||||
|
return 'Починаючи з k=$baselineK на $baselineFreq МГц, обчислення коригує k-фактор для поточного діапазону $frequencyMHz МГц, який визначає викривлену межу радіогоризонту.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contacts_pathTrace => 'Трасування шляхів';
|
String get contacts_pathTrace => 'Трасування шляхів';
|
||||||
|
|
||||||
@@ -2907,4 +3424,282 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get settings_gpxExportShareSubject =>
|
String get settings_gpxExportShareSubject =>
|
||||||
'експорт даних карти meshcore-open у форматі GPX';
|
'експорт даних карти meshcore-open у форматі GPX';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_nearByRepeaters => 'Ближні ретранслятори';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get snrIndicator_lastSeen => 'Останній раз бачили';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_title => 'Налаштування контактів';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddTitle => 'Автоматичне виявлення';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_otherTitle =>
|
||||||
|
'Інші налаштування, пов\'язані з контактами';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersTitle =>
|
||||||
|
'Автоматично додавати користувачів';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddUsersSubtitle =>
|
||||||
|
'Дозволити супутникові автоматично додавати виявлених користувачів';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersTitle =>
|
||||||
|
'Автоматично додавати повторювачі';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||||
|
'Дозволити супутнику автоматично додавати виявлені ретранслятори';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersTitle =>
|
||||||
|
'Автоматично додавати сервери кімнат';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||||
|
'Дозволити супровіднику автоматично додавати виявлені сервери кімнат.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsTitle =>
|
||||||
|
'Автоматично додавати датчики';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||||
|
'Дозволити супровіднику автоматично додавати виявлені сенсори';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestTitle => 'Перезаписати найстаріше';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactsSettings_overwriteOldestSubtitle =>
|
||||||
|
'Коли список контактів заповнений, найстарший контакт без позначки улюбленого буде замінений.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_Title => 'Виявлені контакти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_noMatching =>
|
||||||
|
'Відповідних контактів не знайдено';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_searchHint => 'Знайти виявлені контакти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_contactAdded => 'Контакт додано';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_addContact => 'Додати контакт';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_copyContact =>
|
||||||
|
'Копіювати контакт у буфер обміну';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContact => 'Видалити контакт';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAll =>
|
||||||
|
'Видалити всі виявлені контакти';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveredContacts_deleteContactAllContent =>
|
||||||
|
'Ви впевнені, що хочете видалити всі виявлені контакти?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chat_sendCooldown =>
|
||||||
|
'Будь ласка, зачекайте трохи, перш ніж відправляти знову.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_jumpToOldestUnread =>
|
||||||
|
'Перейти до найстарішого непрочитаного повідомлення';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_jumpToOldestUnreadSubtitle =>
|
||||||
|
'При відкритті чату з не прочитаними повідомленнями, прокрутіть до першого не прочитаного повідомлення, а не до останнього.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageHu => 'Угорський';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageJa => 'Японська';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get appSettings_languageKo => 'Кореєська';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_tooltip => 'Статистика радіо та мережі';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_screenTitle => 'Дані про радіостанції';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_notConnected =>
|
||||||
|
'Підключіться до пристрою, щоб переглядати статистику радіопередач.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_firmwareTooOld =>
|
||||||
|
'Статистика радіо приймача вимагає супутнього програмного забезпечення версії 8 або новішої.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_waiting => 'Очікую на отримання даних…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_noiseFloor(int noiseDbm) {
|
||||||
|
return 'Рівень шуму: $noiseDbm дБм';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_lastRssi(int rssiDbm) {
|
||||||
|
return 'Останній показник RSSI: $rssiDbm дБм';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_lastSnr(String snr) {
|
||||||
|
return 'Останній показник SNR: $snr дБ';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_txAir(int seconds) {
|
||||||
|
return 'Час трансляції на телеканалі TX (загальний): $seconds секунд';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_rxAir(int seconds) {
|
||||||
|
return 'Загальний час використання RX: $seconds секунд';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_chartCaption =>
|
||||||
|
'Рівень шуму (дБм) на основі останніх вимірювань.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String radioStats_stripNoise(int noiseDbm) {
|
||||||
|
return 'Рівень шуму: $noiseDbm дБм';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_stripWaiting => 'Отримано статистику радіо…';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_settingsTile => 'Дані про радіостанції';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get radioStats_settingsSubtitle =>
|
||||||
|
'Рівень шуму, RSSI, SNR та час, протягом якого пристрій використовує радіоканал.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_title => 'Переклад';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_enableTitle => 'Увімкнути переклад';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_enableSubtitle =>
|
||||||
|
'Перекладати отримані повідомлення та дозволяти попередній переклад перед відправкою.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerTitle => 'Перекладіть перед відправкою';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerSubtitle =>
|
||||||
|
'Контролює стан ікон перекладу, який використовується за замовчуванням.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_targetLanguage => 'Цільова мова';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_useAppLanguage => 'Використовуйте мову додатку';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadedModelLabel => 'Завантажений шаблон';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_presetModelLabel =>
|
||||||
|
'Заздалегідь налаштований модель від Hugging Face';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_manualUrlLabel =>
|
||||||
|
'Посилання на веб-сторінку з інструкцією';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadModel => 'Завантажити модель';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloading => 'Завантаження...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_working => 'Працюю...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_stop => 'Припинити';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_mergingChunks =>
|
||||||
|
'Об\'єднання завантажених фрагментів у кінцевий файл...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadedModels => 'Завантажені моделі';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_deleteModel => 'Видалити модель';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_modelDownloaded => 'Модель перекладу завантажена.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_downloadStopped => 'Завантаження призупинено.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String translation_downloadFailed(String error) {
|
||||||
|
return 'Не вдалося завантажити: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_enterUrlFirst => 'Спочатку введіть URL моделі.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_linuxPairingShowPin => 'Показати PIN';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_linuxPairingHidePin => 'Приховати PIN';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanner_linuxPairingPinTitle => 'PIN‑код спарювання Bluetooth';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String scanner_linuxPairingPinPrompt(String deviceName) {
|
||||||
|
return 'Введіть PIN для $deviceName (залиште порожнім, якщо його немає).';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_messageTranslation => 'Переклад повідомлення';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_translateBeforeSending =>
|
||||||
|
'Перекладіть перед відправкою';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerEnabledHint =>
|
||||||
|
'Повідомлення будуть перекладені перед відправленням.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_composerDisabledHint =>
|
||||||
|
'Надсилайте повідомлення, використовуючи оригінальний текстовий формат.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String translation_translateTo(String language) {
|
||||||
|
return 'Перекласти на $language';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_translationOptions => 'Варіанти перекладу';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get translation_systemLanguage => 'Мова системи';
|
||||||
}
|
}
|
||||||
|
|||||||
+1237
-542
File diff suppressed because it is too large
Load Diff
+513
-48
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"channels_channelDeleteFailed": "Kan kanaal {name} niet verwijderen",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@@locale": "nl",
|
"@@locale": "nl",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Contacten",
|
"nav_contacts": "Contacten",
|
||||||
@@ -76,7 +84,7 @@
|
|||||||
"settings_appSettings": "App Instellingen",
|
"settings_appSettings": "App Instellingen",
|
||||||
"settings_appSettingsSubtitle": "Notificaties, berichten en kaartinstellingen",
|
"settings_appSettingsSubtitle": "Notificaties, berichten en kaartinstellingen",
|
||||||
"settings_nodeSettings": "Node Instellingen",
|
"settings_nodeSettings": "Node Instellingen",
|
||||||
"settings_nodeName": "Node Naam",
|
"settings_nodeName": "Nodenaam",
|
||||||
"settings_nodeNameNotSet": "Niet ingesteld",
|
"settings_nodeNameNotSet": "Niet ingesteld",
|
||||||
"settings_nodeNameHint": "Voer nodenaam in",
|
"settings_nodeNameHint": "Voer nodenaam in",
|
||||||
"settings_nodeNameUpdated": "Naam bijgewerkt",
|
"settings_nodeNameUpdated": "Naam bijgewerkt",
|
||||||
@@ -99,13 +107,13 @@
|
|||||||
"settings_sendAdvertisement": "Verzend Advertentie",
|
"settings_sendAdvertisement": "Verzend Advertentie",
|
||||||
"settings_sendAdvertisementSubtitle": "Nu aanwezigheid uitzenden",
|
"settings_sendAdvertisementSubtitle": "Nu aanwezigheid uitzenden",
|
||||||
"settings_advertisementSent": "Advertentie verzonden",
|
"settings_advertisementSent": "Advertentie verzonden",
|
||||||
"settings_syncTime": "Synchronisatie Tijd",
|
"settings_syncTime": "Tijd Synchroniseren",
|
||||||
"settings_syncTimeSubtitle": "Stel de apparaatklok in op de tijd van de telefoon.",
|
"settings_syncTimeSubtitle": "Stel de apparaatklok in op de tijd van de telefoon.",
|
||||||
"settings_timeSynchronized": "Tijdsynchronisatie",
|
"settings_timeSynchronized": "Tijdsynchronisatie",
|
||||||
"settings_refreshContacts": "Contacten vernieuwen",
|
"settings_refreshContacts": "Contacten vernieuwen",
|
||||||
"settings_refreshContactsSubtitle": "Contactlijst opnieuw laden van het apparaat",
|
"settings_refreshContactsSubtitle": "Contactlijst opnieuw laden van het apparaat",
|
||||||
"settings_rebootDevice": "Apparaat opnieuw opstarten",
|
"settings_rebootDevice": "Apparaat opnieuw opstarten",
|
||||||
"settings_rebootDeviceSubtitle": "Herstart het MeshCore apparaat",
|
"settings_rebootDeviceSubtitle": "Herstart het MeshCore-apparaat",
|
||||||
"settings_rebootDeviceConfirm": "Ben je er zeker van dat je het apparaat opnieuw wilt opstarten? Je wordt losgekoppeld.",
|
"settings_rebootDeviceConfirm": "Ben je er zeker van dat je het apparaat opnieuw wilt opstarten? Je wordt losgekoppeld.",
|
||||||
"settings_debug": "Debug",
|
"settings_debug": "Debug",
|
||||||
"settings_bleDebugLog": "BLE Debug Log",
|
"settings_bleDebugLog": "BLE Debug Log",
|
||||||
@@ -137,7 +145,7 @@
|
|||||||
"settings_bandwidth": "Bandbreedte",
|
"settings_bandwidth": "Bandbreedte",
|
||||||
"settings_spreadingFactor": "Spreadsnelheid",
|
"settings_spreadingFactor": "Spreadsnelheid",
|
||||||
"settings_codingRate": "Codeertarief",
|
"settings_codingRate": "Codeertarief",
|
||||||
"settings_txPower": "TX Vermogen (dBm)",
|
"settings_txPower": "TX-Vermogen (dBm)",
|
||||||
"settings_txPowerHelper": "0 - 22",
|
"settings_txPowerHelper": "0 - 22",
|
||||||
"settings_txPowerInvalid": "Ongeldige TX-vermogen (0-22 dBm)",
|
"settings_txPowerInvalid": "Ongeldige TX-vermogen (0-22 dBm)",
|
||||||
"settings_error": "Fout: {message}",
|
"settings_error": "Fout: {message}",
|
||||||
@@ -224,11 +232,11 @@
|
|||||||
"appSettings_mapTimeFilter": "Filter tijd op kaart",
|
"appSettings_mapTimeFilter": "Filter tijd op kaart",
|
||||||
"appSettings_showNodesDiscoveredWithin": "Toon nodes ontdekt binnen:",
|
"appSettings_showNodesDiscoveredWithin": "Toon nodes ontdekt binnen:",
|
||||||
"appSettings_allTime": "Altijd",
|
"appSettings_allTime": "Altijd",
|
||||||
"appSettings_lastHour": "Laat uur",
|
"appSettings_lastHour": "Afgelopen uur",
|
||||||
"appSettings_last6Hours": "laatste 6 uur",
|
"appSettings_last6Hours": "Afgelopen 6 uur",
|
||||||
"appSettings_last24Hours": "De laatste 24 uur",
|
"appSettings_last24Hours": "Afgelopen 24 uur",
|
||||||
"appSettings_lastWeek": "Laatste week",
|
"appSettings_lastWeek": "Afgelopen week",
|
||||||
"appSettings_offlineMapCache": "Offline Kaarten Cache",
|
"appSettings_offlineMapCache": "Offline Kaartcache",
|
||||||
"appSettings_noAreaSelected": "Geen gebied geselecteerd",
|
"appSettings_noAreaSelected": "Geen gebied geselecteerd",
|
||||||
"appSettings_areaSelectedZoom": "Geselecteerd gebied (zoom {minZoom}-{maxZoom})",
|
"appSettings_areaSelectedZoom": "Geselecteerd gebied (zoom {minZoom}-{maxZoom})",
|
||||||
"@appSettings_areaSelectedZoom": {
|
"@appSettings_areaSelectedZoom": {
|
||||||
@@ -277,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": {
|
||||||
@@ -334,6 +343,8 @@
|
|||||||
"channels_publicChannel": "Open kanaal",
|
"channels_publicChannel": "Open kanaal",
|
||||||
"channels_privateChannel": "Private kanaal",
|
"channels_privateChannel": "Private kanaal",
|
||||||
"channels_editChannel": "Kanaal bewerken",
|
"channels_editChannel": "Kanaal bewerken",
|
||||||
|
"channels_muteChannel": "Kanaal dempen",
|
||||||
|
"channels_unmuteChannel": "Kanaal dempen opheffen",
|
||||||
"channels_deleteChannel": "Kanaal verwijderen",
|
"channels_deleteChannel": "Kanaal verwijderen",
|
||||||
"channels_deleteChannelConfirm": "Verwijderen \"{name}\"? Dit kan niet worden teruggedraaid.",
|
"channels_deleteChannelConfirm": "Verwijderen \"{name}\"? Dit kan niet worden teruggedraaid.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
@@ -671,7 +682,7 @@
|
|||||||
"map_showSharedMarkers": "Toon gedeelde markeringen",
|
"map_showSharedMarkers": "Toon gedeelde markeringen",
|
||||||
"map_lastSeenTime": "Laatste Bekeken Tijd",
|
"map_lastSeenTime": "Laatste Bekeken Tijd",
|
||||||
"map_sharedPin": "Gedeelde pin",
|
"map_sharedPin": "Gedeelde pin",
|
||||||
"map_joinRoom": "Sluit Kamer",
|
"map_joinRoom": "Kamer Toetreden",
|
||||||
"map_manageRepeater": "Beheer Repeater",
|
"map_manageRepeater": "Beheer Repeater",
|
||||||
"mapCache_title": "Offline Kaarten Cache",
|
"mapCache_title": "Offline Kaarten Cache",
|
||||||
"mapCache_selectAreaFirst": "Select een gebied om eerst in de cache op te slaan",
|
"mapCache_selectAreaFirst": "Select een gebied om eerst in de cache op te slaan",
|
||||||
@@ -867,7 +878,7 @@
|
|||||||
"path_tooLong": "Pad is te lang. Maximaal 64 sprongen zijn toegestaan.",
|
"path_tooLong": "Pad is te lang. Maximaal 64 sprongen zijn toegestaan.",
|
||||||
"path_setPath": "Stel Pad in",
|
"path_setPath": "Stel Pad in",
|
||||||
"repeater_management": "Beheer Repeaters",
|
"repeater_management": "Beheer Repeaters",
|
||||||
"repeater_managementTools": "Beheerinstrumenten",
|
"repeater_managementTools": "Beheerfuncties",
|
||||||
"repeater_status": "Status",
|
"repeater_status": "Status",
|
||||||
"repeater_statusSubtitle": "Status, statistieken en buren bekijken",
|
"repeater_statusSubtitle": "Status, statistieken en buren bekijken",
|
||||||
"repeater_telemetry": "Telemetry",
|
"repeater_telemetry": "Telemetry",
|
||||||
@@ -901,8 +912,8 @@
|
|||||||
"repeater_lastRssi": "Laatste RSSI",
|
"repeater_lastRssi": "Laatste RSSI",
|
||||||
"repeater_lastSnr": "Laatste SNR",
|
"repeater_lastSnr": "Laatste SNR",
|
||||||
"repeater_noiseFloor": "Ruisvloer",
|
"repeater_noiseFloor": "Ruisvloer",
|
||||||
"repeater_txAirtime": "TX Airtime",
|
"repeater_txAirtime": "TX-zendtijd",
|
||||||
"repeater_rxAirtime": "RX Airtime",
|
"repeater_rxAirtime": "RX-zendtijd",
|
||||||
"repeater_packetStatistics": "Pakketstatistieken",
|
"repeater_packetStatistics": "Pakketstatistieken",
|
||||||
"repeater_sent": "Verzonden",
|
"repeater_sent": "Verzonden",
|
||||||
"repeater_received": "Ontvangen",
|
"repeater_received": "Ontvangen",
|
||||||
@@ -971,14 +982,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_settingsTitle": "Repeater Instellingen",
|
"repeater_settingsTitle": "Repeaterinstellingen",
|
||||||
"repeater_basicSettings": "Basisinstellingen",
|
"repeater_basicSettings": "Basisinstellingen",
|
||||||
"repeater_repeaterName": "Repeaternaam",
|
"repeater_repeaterName": "Repeaternaam",
|
||||||
"repeater_repeaterNameHelper": "Weergave naam voor deze repeater",
|
"repeater_repeaterNameHelper": "Weergavenaam voor deze repeater",
|
||||||
"repeater_adminPassword": "Admin wachtwoord",
|
"repeater_adminPassword": "Admin wachtwoord",
|
||||||
"repeater_adminPasswordHelper": "Volledige toegangspaswoord",
|
"repeater_adminPasswordHelper": "Wachtwoord administratortoegang",
|
||||||
"repeater_guestPassword": "Wachtwoord Gast",
|
"repeater_guestPassword": "Gast wachtwoord",
|
||||||
"repeater_guestPasswordHelper": "Leesbeheer wachtwoord",
|
"repeater_guestPasswordHelper": "Wachtwoord gasttoegen",
|
||||||
"repeater_radioSettings": "Radio Instellingen",
|
"repeater_radioSettings": "Radio Instellingen",
|
||||||
"repeater_frequencyMhz": "Frequentie (MHz)",
|
"repeater_frequencyMhz": "Frequentie (MHz)",
|
||||||
"repeater_frequencyHelper": "300-2500 MHz",
|
"repeater_frequencyHelper": "300-2500 MHz",
|
||||||
@@ -987,7 +998,7 @@
|
|||||||
"repeater_bandwidth": "Bandbreedte",
|
"repeater_bandwidth": "Bandbreedte",
|
||||||
"repeater_spreadingFactor": "Spreidingsfactor",
|
"repeater_spreadingFactor": "Spreidingsfactor",
|
||||||
"repeater_codingRate": "Codeertarief",
|
"repeater_codingRate": "Codeertarief",
|
||||||
"repeater_locationSettings": "Locatie Instellingen",
|
"repeater_locationSettings": "Locatie-instellingen",
|
||||||
"repeater_latitude": "Breedtegraad",
|
"repeater_latitude": "Breedtegraad",
|
||||||
"repeater_latitudeHelper": "Graadseconden (bijv. 37.7749)",
|
"repeater_latitudeHelper": "Graadseconden (bijv. 37.7749)",
|
||||||
"repeater_longitude": "Lengtegraad",
|
"repeater_longitude": "Lengtegraad",
|
||||||
@@ -997,9 +1008,9 @@
|
|||||||
"repeater_packetForwardingSubtitle": "Repeater instellen om pakketten door te sturen",
|
"repeater_packetForwardingSubtitle": "Repeater instellen om pakketten door te sturen",
|
||||||
"repeater_guestAccess": "Toegang voor Gasten",
|
"repeater_guestAccess": "Toegang voor Gasten",
|
||||||
"repeater_guestAccessSubtitle": "Toegestane leesbeheer toegang voor gasten.",
|
"repeater_guestAccessSubtitle": "Toegestane leesbeheer toegang voor gasten.",
|
||||||
"repeater_privacyMode": "Privacy Modus",
|
"repeater_privacyMode": "Privacymodus",
|
||||||
"repeater_privacyModeSubtitle": "Naam/locatie verbergen in advertenties",
|
"repeater_privacyModeSubtitle": "Naam/locatie verbergen in advertenties",
|
||||||
"repeater_advertisementSettings": "Advertentie Instellingen",
|
"repeater_advertisementSettings": "Advertentie-instellingen",
|
||||||
"repeater_localAdvertInterval": "Lokale Advertentie Interval",
|
"repeater_localAdvertInterval": "Lokale Advertentie Interval",
|
||||||
"repeater_localAdvertIntervalMinutes": "{minutes} minuten",
|
"repeater_localAdvertIntervalMinutes": "{minutes} minuten",
|
||||||
"@repeater_localAdvertIntervalMinutes": {
|
"@repeater_localAdvertIntervalMinutes": {
|
||||||
@@ -1062,7 +1073,7 @@
|
|||||||
"repeater_refreshLocationSettings": "Instellingen Locatie Vernieuwen",
|
"repeater_refreshLocationSettings": "Instellingen Locatie Vernieuwen",
|
||||||
"repeater_refreshPacketForwarding": "Vernieuwen Pakket Doorversturing",
|
"repeater_refreshPacketForwarding": "Vernieuwen Pakket Doorversturing",
|
||||||
"repeater_refreshGuestAccess": "Toegang Gast Vernieuwen",
|
"repeater_refreshGuestAccess": "Toegang Gast Vernieuwen",
|
||||||
"repeater_refreshPrivacyMode": "Privacy Mode vernieuwen",
|
"repeater_refreshPrivacyMode": "Privacymode vernieuwen",
|
||||||
"repeater_refreshAdvertisementSettings": "Instellingen Advertentie Bijwerken",
|
"repeater_refreshAdvertisementSettings": "Instellingen Advertentie Bijwerken",
|
||||||
"repeater_refreshed": "{label} is vernieuwd",
|
"repeater_refreshed": "{label} is vernieuwd",
|
||||||
"@repeater_refreshed": {
|
"@repeater_refreshed": {
|
||||||
@@ -1083,8 +1094,8 @@
|
|||||||
"repeater_cliTitle": "Repeater CLI",
|
"repeater_cliTitle": "Repeater CLI",
|
||||||
"repeater_debugNextCommand": "Debug Volgende Commando",
|
"repeater_debugNextCommand": "Debug Volgende Commando",
|
||||||
"repeater_commandHelp": "Help",
|
"repeater_commandHelp": "Help",
|
||||||
"repeater_clearHistory": "Verwijder Geschiedenis",
|
"repeater_clearHistory": "Geschiedenis Verwijderen",
|
||||||
"repeater_noCommandsSent": "Geen commando's verzonden nog.",
|
"repeater_noCommandsSent": "Nog geen commando's verzonden.",
|
||||||
"repeater_typeCommandOrUseQuick": "Typ een opdracht hieronder of gebruik snelle commando's",
|
"repeater_typeCommandOrUseQuick": "Typ een opdracht hieronder of gebruik snelle commando's",
|
||||||
"repeater_enterCommandHint": "Voer bevel in...",
|
"repeater_enterCommandHint": "Voer bevel in...",
|
||||||
"repeater_previousCommand": "Vorige opdracht",
|
"repeater_previousCommand": "Vorige opdracht",
|
||||||
@@ -1099,14 +1110,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_cliQuickGetName": "Haal Naam op",
|
"repeater_cliQuickGetName": "Naam opvragen",
|
||||||
"repeater_cliQuickGetRadio": "Radio ontvangen",
|
"repeater_cliQuickGetRadio": "Radio-instellingen opvragen",
|
||||||
"repeater_cliQuickGetTx": "Krijg TX",
|
"repeater_cliQuickGetTx": "TX opvragen",
|
||||||
"repeater_cliQuickNeighbors": "Buren",
|
"repeater_cliQuickNeighbors": "Buren opvragen",
|
||||||
"repeater_cliQuickVersion": "Versie",
|
"repeater_cliQuickVersion": "Versie opvragen",
|
||||||
"repeater_cliQuickAdvertise": "Advertenties",
|
"repeater_cliQuickAdvertise": "Advertenties opvragen",
|
||||||
"repeater_cliQuickClock": "Tijd",
|
"repeater_cliQuickClock": "Tijd opvragen",
|
||||||
"repeater_cliHelpAdvert": "Verstuurt een advertentiepakket",
|
"repeater_cliHelpAdvert": "Advertentie uitzenden",
|
||||||
"repeater_cliHelpReboot": "Herstart het apparaat. (let op, je krijgt mogelijk een 'Timeout', wat normaal is)",
|
"repeater_cliHelpReboot": "Herstart het apparaat. (let op, je krijgt mogelijk een 'Timeout', wat normaal is)",
|
||||||
"repeater_cliHelpClock": "Toont de huidige tijd per apparaat's klok.",
|
"repeater_cliHelpClock": "Toont de huidige tijd per apparaat's klok.",
|
||||||
"repeater_cliHelpPassword": "Stelt een nieuw beheerderswachtwoord in voor het apparaat.",
|
"repeater_cliHelpPassword": "Stelt een nieuw beheerderswachtwoord in voor het apparaat.",
|
||||||
@@ -1192,7 +1203,7 @@
|
|||||||
},
|
},
|
||||||
"telemetry_batteryLabel": "Batterij",
|
"telemetry_batteryLabel": "Batterij",
|
||||||
"telemetry_voltageLabel": "Spanning",
|
"telemetry_voltageLabel": "Spanning",
|
||||||
"telemetry_mcuTemperatureLabel": "MCU Temperatuur",
|
"telemetry_mcuTemperatureLabel": "MCU-temperatuur",
|
||||||
"telemetry_temperatureLabel": "Temperatuur",
|
"telemetry_temperatureLabel": "Temperatuur",
|
||||||
"telemetry_currentLabel": "Huidig",
|
"telemetry_currentLabel": "Huidig",
|
||||||
"telemetry_batteryValue": "{percent}% / {volts}V",
|
"telemetry_batteryValue": "{percent}% / {volts}V",
|
||||||
@@ -1335,7 +1346,7 @@
|
|||||||
"listFilter_tooltip": "Filteren en sorteren",
|
"listFilter_tooltip": "Filteren en sorteren",
|
||||||
"listFilter_sortBy": "Sorteren door",
|
"listFilter_sortBy": "Sorteren door",
|
||||||
"listFilter_latestMessages": "Recente berichten",
|
"listFilter_latestMessages": "Recente berichten",
|
||||||
"listFilter_heardRecently": "Hoor je onlangs",
|
"listFilter_heardRecently": "Recent gezien",
|
||||||
"listFilter_az": "A-Z",
|
"listFilter_az": "A-Z",
|
||||||
"listFilter_filters": "Filters",
|
"listFilter_filters": "Filters",
|
||||||
"listFilter_all": "Alles",
|
"listFilter_all": "Alles",
|
||||||
@@ -1351,21 +1362,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Buren",
|
"repeater_neighbors": "Buren",
|
||||||
"repeater_neighboursSubtitle": "Bekijk nul hops buren.",
|
"repeater_neighborsSubtitle": "Bekijk nul-hopsburen.",
|
||||||
"neighbors_receivedData": "Ontvangen Buurdata",
|
"neighbors_receivedData": "Ontvangen Buurdata",
|
||||||
"neighbors_requestTimedOut": "Buren vragen om tijdelijk uitgeschakeld.",
|
"neighbors_requestTimedOut": "Buren vragen om tijdelijk uitgeschakeld.",
|
||||||
"neighbors_errorLoading": "Fout bij het laden van buren: {error}",
|
"neighbors_errorLoading": "Fout bij het laden van buren: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Herhalingen Buren",
|
"neighbors_repeatersNeighbors": "Repeatbburen",
|
||||||
"neighbors_noData": "Geen gegevens van buren beschikbaar.",
|
"neighbors_noData": "Geen gegevens van buren beschikbaar.",
|
||||||
"channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.",
|
"channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.",
|
||||||
"channels_createPrivateChannel": "Maak een Privé Kanaal",
|
"channels_createPrivateChannel": "PrivéKanaal Aanmaken",
|
||||||
"channels_joinPrivateChannel": "Sluit een Privé Kanaal aan",
|
"channels_joinPrivateChannel": "PrivéKanaal Toetreden",
|
||||||
"channels_joinPrivateChannelDesc": "Handmatig een geheime sleutel invoeren.",
|
"channels_joinPrivateChannelDesc": "Voer handmatig een geheime sleutel in.",
|
||||||
"channels_joinPublicChannel": "Sluit het Open Kanaal",
|
"channels_joinPublicChannel": "Publiek Kanaal Toetreden",
|
||||||
"channels_joinPublicChannelDesc": "Iedereen kan dit kanaal aanmelden.",
|
"channels_joinPublicChannelDesc": "Iedereen kan toetreden tot dit kanaal.",
|
||||||
"channels_joinHashtagChannel": "Sluit een Hashtag Kanaal",
|
"channels_joinHashtagChannel": "Hashtag-kanaal Aanmaken",
|
||||||
"channels_joinHashtagChannelDesc": "Iedereen kan lid worden van hashtag-kanalen.",
|
"channels_joinHashtagChannelDesc": "Iedereen kan toetreden tot hashtag-kanalen.",
|
||||||
"channels_scanQrCode": "Scan een QR-code",
|
"channels_scanQrCode": "Scan een QR-code",
|
||||||
"channels_scanQrCodeComingSoon": "Komt later",
|
"channels_scanQrCodeComingSoon": "Komt later",
|
||||||
"channels_enterHashtag": "Voer hashtag in",
|
"channels_enterHashtag": "Voer hashtag in",
|
||||||
@@ -1555,13 +1566,15 @@
|
|||||||
"contacts_floodAdvert": "Overstromingsadvertentie",
|
"contacts_floodAdvert": "Overstromingsadvertentie",
|
||||||
"contacts_copyAdvertToClipboard": "Advert naar klembord kopiëren",
|
"contacts_copyAdvertToClipboard": "Advert naar klembord kopiëren",
|
||||||
"appSettings_languageRu": "Russisch",
|
"appSettings_languageRu": "Russisch",
|
||||||
|
"appSettings_enableMessageTracing": "Berichttracking inschakelen",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Gedetailleerde routerings- en timing-metadata voor berichten weergeven",
|
||||||
"contacts_clipboardEmpty": "Knipbord is leeg.",
|
"contacts_clipboardEmpty": "Knipbord is leeg.",
|
||||||
"contacts_addContactFromClipboard": "Contact uit klembord toevoegen",
|
"contacts_addContactFromClipboard": "Contact uit klembord toevoegen",
|
||||||
"contacts_contactImported": "Contact is geïmporteerd.",
|
"contacts_contactImported": "Contact is geïmporteerd.",
|
||||||
"contacts_zeroHopContactAdvertSent": "Contact verzonden via advertentie",
|
"contacts_zeroHopContactAdvertSent": "Contact verzonden via advertentie",
|
||||||
"contacts_contactAdvertCopied": "Reclame gekopieerd naar Klembord.",
|
"contacts_contactAdvertCopied": "Reclame gekopieerd naar Klembord.",
|
||||||
"contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.",
|
"contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.",
|
||||||
"contacts_ShareContact": "Kontakt naar Klembord kopiëren",
|
"contacts_ShareContact": "Contact naar Klembord kopiëren",
|
||||||
"contacts_ShareContactZeroHop": "Contact delen via advertentie",
|
"contacts_ShareContactZeroHop": "Contact delen via advertentie",
|
||||||
"contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden",
|
"contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden",
|
||||||
"notification_activityTitle": "MeshCore Activiteit",
|
"notification_activityTitle": "MeshCore Activiteit",
|
||||||
@@ -1593,8 +1606,460 @@
|
|||||||
"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",
|
||||||
"settings_clientRepeat": "Herhalen: Afgekoppeld",
|
"snrIndicator_lastSeen": "Laatst gezien",
|
||||||
"settings_clientRepeatSubtitle": "Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.",
|
"snrIndicator_nearByRepeaters": "Nabije herhalingseenheden",
|
||||||
"settings_clientRepeatFreqWarning": "Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist."
|
"chat_ShowAllPaths": "Toon alle paden",
|
||||||
|
"settings_clientRepeat": "Off-Grid Herhalen",
|
||||||
|
"settings_clientRepeatSubtitle": "Laat dit apparaat de berichten van andere apparaten doorsturen.",
|
||||||
|
"settings_clientRepeatFreqWarning": "Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist.",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "LOS-hoogtegegevens: Open-Meteo (CC BY 4.0)",
|
||||||
|
"appSettings_unitsTitle": "Eenheden",
|
||||||
|
"appSettings_unitsMetric": "Metrisch (m / km)",
|
||||||
|
"appSettings_unitsImperial": "Imperiaal (ft / mi)",
|
||||||
|
"map_lineOfSight": "Zichtlijn",
|
||||||
|
"map_losScreenTitle": "Zichtlijn",
|
||||||
|
"losSelectStartEnd": "Selecteer begin- en eindknooppunten voor LOS.",
|
||||||
|
"losRunFailed": "Zichtlijncontrole mislukt: {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Wis alle punten",
|
||||||
|
"losRunToViewElevationProfile": "Voer LOS uit om het hoogteprofiel te bekijken",
|
||||||
|
"losMenuTitle": "LOS-menu",
|
||||||
|
"losMenuSubtitle": "Tik op knooppunten of druk lang op de kaart voor aangepaste punten",
|
||||||
|
"losShowDisplayNodes": "Toon weergaveknooppunten",
|
||||||
|
"losCustomPoints": "Aangepaste punten",
|
||||||
|
"losCustomPointLabel": "Aangepast {index}",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Punt A",
|
||||||
|
"losPointB": "Punt B",
|
||||||
|
"losAntennaA": "Antenne A: {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Antenne B: {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Voer LOS uit",
|
||||||
|
"losNoElevationData": "Geen hoogtegegevens",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, vrije LOS, min. vrije ruimte {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, geblokkeerd door {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "LOS: controleren...",
|
||||||
|
"losStatusNoData": "LOS: geen gegevens",
|
||||||
|
"losStatusSummary": "LOS: {clear}/{total} gewist, {blocked} geblokkeerd, {unknown} onbekend",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Hoogtegegevens niet beschikbaar voor een of meer monsters.",
|
||||||
|
"losErrorInvalidInput": "Ongeldige punten/hoogtegegevens voor LOS-berekening.",
|
||||||
|
"losRenameCustomPoint": "Hernoem aangepast punt",
|
||||||
|
"losPointName": "Puntnaam",
|
||||||
|
"losShowPanelTooltip": "Toon LOS-paneel",
|
||||||
|
"losHidePanelTooltip": "LOS-paneel verbergen",
|
||||||
|
"losElevationAttribution": "Hoogtegegevens: Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Radiohorizon",
|
||||||
|
"losLegendLosBeam": "Zichtlijn",
|
||||||
|
"losLegendTerrain": "Terrein",
|
||||||
|
"losFrequencyLabel": "Frequentie",
|
||||||
|
"losFrequencyInfoTooltip": "Bekijk details van de berekening",
|
||||||
|
"losFrequencyDialogTitle": "Berekening van de radiohorizon",
|
||||||
|
"losFrequencyDialogDescription": "Beginnend met k={baselineK} bij {baselineFreq} MHz, wordt bij de berekening de k-factor aangepast voor de huidige {frequencyMHz} MHz-band, die de gebogen radiohorizonkap definieert.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listFilter_removeFromFavorites": "Verwijderen uit favorieten",
|
||||||
|
"listFilter_favorites": "Favorieten",
|
||||||
|
"listFilter_addToFavorites": "Toevoegen aan favorieten",
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_unread": "Ongelezen",
|
||||||
|
"contacts_searchRepeaters": "Zoek {number}{str} Repeaters...",
|
||||||
|
"contacts_searchContactsNoNumber": "Zoek contacten...",
|
||||||
|
"contacts_searchUsers": "Zoek {number}{str} gebruikers...",
|
||||||
|
"contacts_searchFavorites": "Zoek {number}{str} favorieten...",
|
||||||
|
"contacts_searchRoomServers": "Zoek {number}{str} Room servers...",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Gebruikers automatisch toevoegen",
|
||||||
|
"contactsSettings_title": "Instellingen voor contacten",
|
||||||
|
"settings_contactSettings": "Contactinstellingen",
|
||||||
|
"contactsSettings_otherTitle": "Andere instellingen voor contactgerelateerde zaken",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Sta toe dat de companion automatisch ontdekte repeaters toevoegt",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Automatisch kamerservers toevoegen",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Sta toe dat de companion automatisch ontdekte kamer servers toevoegt.",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Automatisch sensoren toevoegen",
|
||||||
|
"settings_contactSettingsSubtitle": "Instellingen voor het toevoegen van contacten",
|
||||||
|
"contactsSettings_autoAddTitle": "Automatische detectie",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Sta toe dat de companion automatisch ontdekte sensoren toevoegt",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Sta toe dat de companion automatisch ontdekte gebruikers toevoegt",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Automatisch herhalingstoestellen toevoegen",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Overschrijf Oudste",
|
||||||
|
"discoveredContacts_noMatching": "Geen overeenkomende contacten",
|
||||||
|
"discoveredContacts_addContact": "Contact toevoegen",
|
||||||
|
"discoveredContacts_copyContact": "Kopieer contact naar klembord",
|
||||||
|
"discoveredContacts_deleteContact": "Contact verwijderen",
|
||||||
|
"discoveredContacts_Title": "Ontdekte contacten",
|
||||||
|
"discoveredContacts_contactAdded": "Contact toegevoegd",
|
||||||
|
"discoveredContacts_searchHint": "Ontdekte contacten zoeken",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Wanneer de contactenlijst vol is, wordt de oudste niet-favoriete contactpersoon vervangen.",
|
||||||
|
"common_deleteAll": "Alles verwijderen",
|
||||||
|
"discoveredContacts_deleteContactAll": "Verwijder alle ontdekte contacten",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Weet u zeker dat u alle ontdekte contacten wilt verwijderen?",
|
||||||
|
"map_guessedLocation": "Geroerde locatie",
|
||||||
|
"map_showGuessedLocations": "Toon de voorspelde locaties van de knopen",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenSubtitle": "Selecteer een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenTitle": "Verbind via USB",
|
||||||
|
"usbScreenStatus": "Selecteer een USB-apparaat",
|
||||||
|
"usbScreenNote": "USB-serieel is actief op ondersteunde Android-apparaten en desktop-platforms.",
|
||||||
|
"usbScreenEmptyState": "Geen USB-apparaten gevonden. Sluit er een aan en herlaad.",
|
||||||
|
"usbErrorPermissionDenied": "Toegang via USB is geweigerd.",
|
||||||
|
"usbErrorDeviceMissing": "Het geselecteerde USB-apparaat is niet meer beschikbaar.",
|
||||||
|
"usbErrorInvalidPort": "Selecteer een geldig USB-apparaat.",
|
||||||
|
"usbErrorBusy": "Een andere verzoek om een USB-verbinding is al in behandeling.",
|
||||||
|
"usbErrorNotConnected": "Er is geen USB-apparaat aangesloten.",
|
||||||
|
"usbErrorOpenFailed": "Kon het geselecteerde USB-apparaat niet openen.",
|
||||||
|
"usbErrorConnectFailed": "Kon niet verbinding maken met het geselecteerde USB-apparaat.",
|
||||||
|
"usbErrorUnsupported": "USB-serieel is niet ondersteund op deze platform.",
|
||||||
|
"usbErrorAlreadyActive": "Een USB-verbinding is al actief.",
|
||||||
|
"usbErrorNoDeviceSelected": "Geen USB-apparaat is geselecteerd.",
|
||||||
|
"usbErrorPortClosed": "De USB-verbinding is niet actief.",
|
||||||
|
"usbFallbackDeviceName": "Web-serieapparaat",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbConnectionFailed": "Fout bij de USB-verbinding: {error}",
|
||||||
|
"usbStatus_notConnected": "Selecteer een USB-apparaat",
|
||||||
|
"usbStatus_connecting": "Verbinding maken met USB-apparaat...",
|
||||||
|
"usbStatus_searching": "Zoeken naar USB-apparaten...",
|
||||||
|
"usbErrorConnectTimedOut": "Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft.",
|
||||||
|
"@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.",
|
||||||
|
"@radioStats_noiseFloor": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_lastRssi": {
|
||||||
|
"placeholders": {
|
||||||
|
"rssiDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_lastSnr": {
|
||||||
|
"placeholders": {
|
||||||
|
"snr": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_txAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_rxAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_stripNoise": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"appSettings_jumpToOldestUnread": "Ga naar het oudste ongelezen bericht",
|
||||||
|
"appSettings_jumpToOldestUnreadSubtitle": "Bij het openen van een chat met ongelezen berichten, scroll dan naar het eerste ongelezen bericht, in plaats van naar het meest recente.",
|
||||||
|
"chat_sendCooldown": "Gelieve even te wachten voordat u opnieuw verzendt.",
|
||||||
|
"appSettings_languageHu": "Hongaars",
|
||||||
|
"appSettings_languageJa": "Japanisch",
|
||||||
|
"appSettings_languageKo": "Koreaans",
|
||||||
|
"radioStats_tooltip": "Statistieken voor radio en mesh-netwerken",
|
||||||
|
"radioStats_screenTitle": "Statistieken over radio",
|
||||||
|
"radioStats_notConnected": "Verbind met een apparaat om radio-statistieken te bekijken.",
|
||||||
|
"radioStats_firmwareTooOld": "Om de statistieken via radio te kunnen gebruiken, is firmware versie 8 of een nieuwere vereist.",
|
||||||
|
"radioStats_waiting": "Wacht op gegevens…",
|
||||||
|
"radioStats_noiseFloor": "Ruisfrequentie: {noiseDbm} dBm",
|
||||||
|
"radioStats_lastRssi": "Laatste RSSI-waarde: {rssiDbm} dBm",
|
||||||
|
"radioStats_lastSnr": "Laatste SNR: {snr} dB",
|
||||||
|
"radioStats_txAir": "TX-tijd (totaal): {seconds} s",
|
||||||
|
"radioStats_rxAir": "Tijd besteed met RX (totaal): {seconds} s",
|
||||||
|
"radioStats_chartCaption": "Ruisfrequentie (dBm) over recente metingen.",
|
||||||
|
"radioStats_stripNoise": "Ruisfrequentie: {noiseDbm} dBm",
|
||||||
|
"radioStats_stripWaiting": "Radio-statistieken ophalen…",
|
||||||
|
"radioStats_settingsTile": "Statistieken over radio",
|
||||||
|
"radioStats_settingsSubtitle": "Ruimtelijke ruis, RSSI, SNR en beschikbare tijd",
|
||||||
|
"@translation_downloadFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_enableSubtitle": "Vertaal inkomende berichten en maak het mogelijk om berichten vooraf te vertalen.",
|
||||||
|
"translation_enableTitle": "Activeer vertaling",
|
||||||
|
"translation_title": "Vertaling",
|
||||||
|
"translation_composerTitle": "Vertaal voor verzending",
|
||||||
|
"translation_composerSubtitle": "Stelt de standaardstatus van het pictogram voor de vertaling van de componist in.",
|
||||||
|
"translation_useAppLanguage": "Gebruik de taal van de app",
|
||||||
|
"translation_targetLanguage": "Doeltaal",
|
||||||
|
"translation_downloadedModelLabel": "Gedownloade model",
|
||||||
|
"translation_presetModelLabel": "Voorgeprogrammeerd Hugging Face-model",
|
||||||
|
"translation_manualUrlLabel": "URL van de handleiding",
|
||||||
|
"translation_downloadModel": "Download het model",
|
||||||
|
"translation_downloading": "Downloaden...",
|
||||||
|
"translation_working": "Werken...",
|
||||||
|
"translation_mergingChunks": "Het samenvoegen van de gedownloade stukken tot één eindbestand...",
|
||||||
|
"translation_stop": "Stoppen",
|
||||||
|
"translation_downloadedModels": "Gedownloade modellen",
|
||||||
|
"translation_deleteModel": "Model verwijderen",
|
||||||
|
"translation_modelDownloaded": "Vertalingmodel gedownload.",
|
||||||
|
"translation_downloadStopped": "Download is afgebroken.",
|
||||||
|
"translation_downloadFailed": "Download mislukt: {error}",
|
||||||
|
"translation_enterUrlFirst": "Voer eerst een URL van een model in.",
|
||||||
|
"@scanner_linuxPairingPinPrompt": {
|
||||||
|
"placeholders": {
|
||||||
|
"deviceName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@translation_translateTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"language": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_composerDisabledHint": "Stuur berichten in de oorspronkelijke, getypte taal.",
|
||||||
|
"translation_translateBeforeSending": "Vertaal voor verzending",
|
||||||
|
"translation_composerEnabledHint": "De berichten worden vertaald voordat ze verzonden worden.",
|
||||||
|
"translation_messageTranslation": "Berichtvertaling",
|
||||||
|
"translation_translationOptions": "Opties voor vertaling",
|
||||||
|
"translation_systemLanguage": "Taal van het systeem",
|
||||||
|
"translation_translateTo": "Vertalen naar {language}",
|
||||||
|
"scanner_linuxPairingShowPin": "Toon PIN",
|
||||||
|
"scanner_linuxPairingHidePin": "PIN verbergen",
|
||||||
|
"scanner_linuxPairingPinPrompt": "Voer PIN in voor {deviceName} (laat leeg als er geen is).",
|
||||||
|
"scanner_linuxPairingPinTitle": "Bluetooth‑koppelings‑PIN",
|
||||||
|
"repeater_cliQuickDiscovery": "Ontdek Buren",
|
||||||
|
"repeater_cliQuickClockSync": "Kloksynchronisatie"
|
||||||
}
|
}
|
||||||
+719
-216
File diff suppressed because it is too large
Load Diff
+469
-4
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"channels_channelDeleteFailed": "Falha ao excluir o canal \"{name}\"",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@@locale": "pt",
|
"@@locale": "pt",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Contactos",
|
"nav_contacts": "Contactos",
|
||||||
@@ -277,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": {
|
||||||
@@ -334,6 +343,8 @@
|
|||||||
"channels_publicChannel": "Canal público",
|
"channels_publicChannel": "Canal público",
|
||||||
"channels_privateChannel": "Canal privado",
|
"channels_privateChannel": "Canal privado",
|
||||||
"channels_editChannel": "Editar canal",
|
"channels_editChannel": "Editar canal",
|
||||||
|
"channels_muteChannel": "Silenciar canal",
|
||||||
|
"channels_unmuteChannel": "Ativar canal",
|
||||||
"channels_deleteChannel": "Excluir canal",
|
"channels_deleteChannel": "Excluir canal",
|
||||||
"channels_deleteChannelConfirm": "Excluir \"{name}\"? Não pode ser desfeito.",
|
"channels_deleteChannelConfirm": "Excluir \"{name}\"? Não pode ser desfeito.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
@@ -1351,12 +1362,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Vizinhos",
|
"repeater_neighbors": "Vizinhos",
|
||||||
"neighbors_receivedData": "Dados dos Vizinhos Recebidos",
|
"neighbors_receivedData": "Dados dos Vizinhos Recebidos",
|
||||||
"repeater_neighboursSubtitle": "Visualizar vizinhos de salto zero.",
|
"repeater_neighborsSubtitle": "Visualizar vizinhos de salto zero.",
|
||||||
"neighbors_requestTimedOut": "Vizinhos solicitam tempo limite esgotado.",
|
"neighbors_requestTimedOut": "Vizinhos solicitam tempo limite esgotado.",
|
||||||
"neighbors_errorLoading": "Erro ao carregar vizinhos: {error}",
|
"neighbors_errorLoading": "Erro ao carregar vizinhos: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Repetidores Vizinhos",
|
"neighbors_repeatersNeighbors": "Repetidores Vizinhos",
|
||||||
"neighbors_noData": "Não estão disponíveis dados de vizinhos.",
|
"neighbors_noData": "Não estão disponíveis dados de vizinhos.",
|
||||||
"channels_createPrivateChannelDesc": "Protegido com uma chave secreta.",
|
"channels_createPrivateChannelDesc": "Protegido com uma chave secreta.",
|
||||||
"channels_joinPrivateChannelDesc": "Inserir uma chave secreta manualmente.",
|
"channels_joinPrivateChannelDesc": "Inserir uma chave secreta manualmente.",
|
||||||
@@ -1556,6 +1567,8 @@
|
|||||||
"contacts_copyAdvertToClipboard": "Copiar Anúncio para Área de Transferência",
|
"contacts_copyAdvertToClipboard": "Copiar Anúncio para Área de Transferência",
|
||||||
"contacts_addContactFromClipboard": "Adicionar Contato da Área de Transferência",
|
"contacts_addContactFromClipboard": "Adicionar Contato da Área de Transferência",
|
||||||
"appSettings_languageRu": "Russo",
|
"appSettings_languageRu": "Russo",
|
||||||
|
"appSettings_enableMessageTracing": "Ativar rastreamento de mensagens",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Mostrar metadados detalhados de roteamento e tempo para as mensagens",
|
||||||
"contacts_ShareContact": "Copiar contato para Área de Transferência",
|
"contacts_ShareContact": "Copiar contato para Área de Transferência",
|
||||||
"contacts_contactImportFailed": "Contato falhou ao ser importado.",
|
"contacts_contactImportFailed": "Contato falhou ao ser importado.",
|
||||||
"contacts_zeroHopContactAdvertSent": "Enviou contato por anúncio.",
|
"contacts_zeroHopContactAdvertSent": "Enviou contato por anúncio.",
|
||||||
@@ -1594,7 +1607,459 @@
|
|||||||
"scanner_enableBluetooth": "Ative o Bluetooth",
|
"scanner_enableBluetooth": "Ative o Bluetooth",
|
||||||
"scanner_bluetoothOff": "Bluetooth está desativado",
|
"scanner_bluetoothOff": "Bluetooth está desativado",
|
||||||
"scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos.",
|
"scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos.",
|
||||||
|
"scanner_chromeRequired": "Navegador Chrome necessário",
|
||||||
|
"scanner_chromeRequiredMessage": "Esta aplicação web requer o Google Chrome ou um navegador baseado no Chromium para suporte de Bluetooth.",
|
||||||
|
"snrIndicator_nearByRepeaters": "Repetidores Próximos",
|
||||||
|
"snrIndicator_lastSeen": "Visto pela última vez",
|
||||||
|
"chat_ShowAllPaths": "Mostrar todos os caminhos",
|
||||||
"settings_clientRepeatFreqWarning": "A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.",
|
"settings_clientRepeatFreqWarning": "A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.",
|
||||||
"settings_clientRepeat": "Repetição sem rede",
|
"settings_clientRepeat": "Repetição sem rede",
|
||||||
"settings_clientRepeatSubtitle": "Permita que este dispositivo repita pacotes de rede para outros dispositivos."
|
"settings_clientRepeatSubtitle": "Permita que este dispositivo repita pacotes de rede para outros dispositivos.",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "Dados de elevação LOS: Open-Meteo (CC BY 4.0)",
|
||||||
|
"appSettings_unitsTitle": "Unidades",
|
||||||
|
"appSettings_unitsMetric": "Métrico (m/km)",
|
||||||
|
"appSettings_unitsImperial": "Imperial (ft/mi)",
|
||||||
|
"map_lineOfSight": "Linha de visão",
|
||||||
|
"map_losScreenTitle": "Linha de visão",
|
||||||
|
"losSelectStartEnd": "Selecione nós iniciais e finais para LOS.",
|
||||||
|
"losRunFailed": "Falha na verificação da linha de visão: {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Limpe todos os pontos",
|
||||||
|
"losRunToViewElevationProfile": "Execute o LOS para visualizar o perfil de elevação",
|
||||||
|
"losMenuTitle": "Menu LOS",
|
||||||
|
"losMenuSubtitle": "Toque nos nós ou mantenha pressionado o mapa para obter pontos personalizados",
|
||||||
|
"losShowDisplayNodes": "Mostrar nós de exibição",
|
||||||
|
"losCustomPoints": "Pontos personalizados",
|
||||||
|
"losCustomPointLabel": "{index} personalizado",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Ponto A",
|
||||||
|
"losPointB": "Ponto B",
|
||||||
|
"losAntennaA": "Antena A: {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Antena B: {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Executar LOS",
|
||||||
|
"losNoElevationData": "Sem dados de elevação",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, limpar LOS, liberação mínima {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, bloqueado por {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "LOS: verificando...",
|
||||||
|
"losStatusNoData": "LOS: sem dados",
|
||||||
|
"losStatusSummary": "LOS: {clear}/{total} limpo, {blocked} bloqueado, {unknown} desconhecido",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Dados de elevação indisponíveis para uma ou mais amostras.",
|
||||||
|
"losErrorInvalidInput": "Dados de pontos/elevação inválidos para cálculo de LOS.",
|
||||||
|
"losRenameCustomPoint": "Renomear ponto personalizado",
|
||||||
|
"losPointName": "Nome do ponto",
|
||||||
|
"losShowPanelTooltip": "Mostrar painel LOS",
|
||||||
|
"losHidePanelTooltip": "Ocultar painel LOS",
|
||||||
|
"losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Horizonte de rádio",
|
||||||
|
"losLegendLosBeam": "Linha de visada",
|
||||||
|
"losLegendTerrain": "Terreno",
|
||||||
|
"losFrequencyLabel": "Frequência",
|
||||||
|
"losFrequencyInfoTooltip": "Ver detalhes do cálculo",
|
||||||
|
"losFrequencyDialogTitle": "Cálculo do horizonte de rádio",
|
||||||
|
"losFrequencyDialogDescription": "Começando em k={baselineK} em {baselineFreq} MHz, o cálculo ajusta o fator k para a banda atual de {frequencyMHz} MHz, que define o limite do horizonte de rádio curvo.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listFilter_addToFavorites": "Adicionar aos favoritos",
|
||||||
|
"listFilter_removeFromFavorites": "Remover da lista de favoritos",
|
||||||
|
"listFilter_favorites": "Favoritos",
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_searchRepeaters": "Pesquisar {number}{str} Repetidores...",
|
||||||
|
"contacts_searchFavorites": "Pesquisar {number}{str} Favoritos...",
|
||||||
|
"contacts_searchUsers": "Pesquisar {number}{str} Usuários...",
|
||||||
|
"contacts_searchContactsNoNumber": "Pesquisar Contatos...",
|
||||||
|
"contacts_unread": "Não lido",
|
||||||
|
"contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala...",
|
||||||
|
"settings_contactSettings": "Configurações de Contato",
|
||||||
|
"contactsSettings_otherTitle": "Outras configurações relacionadas a contatos",
|
||||||
|
"contactsSettings_title": "Configurações de contatos",
|
||||||
|
"contactsSettings_autoAddTitle": "Descoberta Automática",
|
||||||
|
"settings_contactSettingsSubtitle": "Configurações para como os contatos são adicionados",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Adicionar usuários automaticamente",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Permitir que o companheiro adicione automaticamente os repetidores descobertos.",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Adicionar automaticamente servidores de sala",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Sobrescrever o Mais Antigo",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Adicionar sensores automaticamente",
|
||||||
|
"discoveredContacts_Title": "Contatos Descobertos",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Permitir que o companheiro adicione automaticamente os usuários descobertos.",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Adicionar repetidores automaticamente",
|
||||||
|
"discoveredContacts_noMatching": "Nenhum contato correspondente",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Permitir que o companheiro adicione automaticamente os servidores de salas descobertos.",
|
||||||
|
"discoveredContacts_searchHint": "Pesquisar contatos descobertos",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Permitir que o companheiro adicione automaticamente sensores descobertos.",
|
||||||
|
"discoveredContacts_copyContact": "Copiar Contato para a área de transferência",
|
||||||
|
"discoveredContacts_deleteContact": "Excluir Contato",
|
||||||
|
"discoveredContacts_contactAdded": "Contato adicionado",
|
||||||
|
"discoveredContacts_addContact": "Adicionar Contato",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Quando a lista de contatos estiver cheia, o contato mais antigo não favoritado será substituído.",
|
||||||
|
"common_deleteAll": "Excluir Tudo",
|
||||||
|
"discoveredContacts_deleteContactAll": "Excluir Todos os Contatos Descobertos",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Tem certeza de que deseja excluir todos os contatos descobertos?",
|
||||||
|
"map_guessedLocation": "Localização estimada",
|
||||||
|
"map_showGuessedLocations": "Mostrar as localizações dos nós estimados",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenTitle": "Conecte via USB",
|
||||||
|
"usbScreenSubtitle": "Selecione um dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.",
|
||||||
|
"usbScreenStatus": "Selecione um dispositivo USB",
|
||||||
|
"usbScreenNote": "A comunicação serial via USB está ativa em dispositivos Android e plataformas de desktop compatíveis.",
|
||||||
|
"usbScreenEmptyState": "Nenhum dispositivo USB encontrado. Conecte um e atualize.",
|
||||||
|
"usbErrorPermissionDenied": "A permissão para acesso via USB foi negada.",
|
||||||
|
"usbErrorDeviceMissing": "O dispositivo USB selecionado não está mais disponível.",
|
||||||
|
"usbErrorInvalidPort": "Selecione um dispositivo USB válido.",
|
||||||
|
"usbErrorBusy": "Já existe uma solicitação de conexão USB em andamento.",
|
||||||
|
"usbErrorNotConnected": "Não há nenhum dispositivo USB conectado.",
|
||||||
|
"usbErrorOpenFailed": "Não foi possível abrir o dispositivo USB selecionado.",
|
||||||
|
"usbErrorConnectFailed": "Não foi possível conectar ao dispositivo USB selecionado.",
|
||||||
|
"usbErrorUnsupported": "A comunicação serial via USB não é suportada nesta plataforma.",
|
||||||
|
"usbErrorAlreadyActive": "A conexão USB já está ativa.",
|
||||||
|
"usbErrorNoDeviceSelected": "Nenhum dispositivo USB foi selecionado.",
|
||||||
|
"usbErrorPortClosed": "A conexão USB não está ativa.",
|
||||||
|
"usbFallbackDeviceName": "Dispositivo de Serial para a Web",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_searching": "Procurando por dispositivos USB...",
|
||||||
|
"usbStatus_notConnected": "Selecione um dispositivo USB",
|
||||||
|
"usbConnectionFailed": "Falha na conexão USB: {error}",
|
||||||
|
"usbStatus_connecting": "Conectando ao dispositivo USB...",
|
||||||
|
"usbErrorConnectTimedOut": "A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion.",
|
||||||
|
"@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.",
|
||||||
|
"@radioStats_noiseFloor": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_lastRssi": {
|
||||||
|
"placeholders": {
|
||||||
|
"rssiDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_lastSnr": {
|
||||||
|
"placeholders": {
|
||||||
|
"snr": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_txAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_rxAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_stripNoise": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"appSettings_jumpToOldestUnread": "Vá para a mensagem mais antiga não lida",
|
||||||
|
"chat_sendCooldown": "Por favor, aguarde um momento antes de reenviar.",
|
||||||
|
"appSettings_languageHu": "Húngaro",
|
||||||
|
"appSettings_jumpToOldestUnreadSubtitle": "Ao abrir uma conversa com mensagens não lidas, role para a primeira mensagem não lida, em vez da mais recente.",
|
||||||
|
"appSettings_languageJa": "Japonês",
|
||||||
|
"appSettings_languageKo": "Coreano",
|
||||||
|
"radioStats_tooltip": "Estatísticas de rádio e malha",
|
||||||
|
"radioStats_screenTitle": "Estatísticas de rádio",
|
||||||
|
"radioStats_notConnected": "Conecte-se a um dispositivo para visualizar estatísticas de rádio.",
|
||||||
|
"radioStats_firmwareTooOld": "As estatísticas de rádio exigem o firmware v8 ou uma versão mais recente.",
|
||||||
|
"radioStats_waiting": "Aguardando dados…",
|
||||||
|
"radioStats_noiseFloor": "Nível de ruído: {noiseDbm} dBm",
|
||||||
|
"radioStats_lastRssi": "Último RSSI: {rssiDbm} dBm",
|
||||||
|
"radioStats_lastSnr": "Último SNR: {snr} dB",
|
||||||
|
"radioStats_txAir": "Tempo de transmissão da TX (total): {seconds} s",
|
||||||
|
"radioStats_rxAir": "Tempo de uso do RX (total): {seconds} s",
|
||||||
|
"radioStats_chartCaption": "Nível de ruído (dBm) em amostras recentes.",
|
||||||
|
"radioStats_stripNoise": "Nível de ruído: {noiseDbm} dBm",
|
||||||
|
"radioStats_stripWaiting": "Obtendo estatísticas de rádio…",
|
||||||
|
"radioStats_settingsTile": "Estatísticas de rádio",
|
||||||
|
"radioStats_settingsSubtitle": "Nível de ruído, RSSI, SNR e tempo de transmissão",
|
||||||
|
"@translation_downloadFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_composerTitle": "Traduza antes de enviar",
|
||||||
|
"translation_enableSubtitle": "Traduzir mensagens recebidas e permitir a tradução antes do envio.",
|
||||||
|
"translation_enableTitle": "Ativar a tradução",
|
||||||
|
"translation_title": "Tradução",
|
||||||
|
"translation_composerSubtitle": "Controla o estado padrão do ícone de tradução do compositor.",
|
||||||
|
"translation_targetLanguage": "Língua-alvo",
|
||||||
|
"translation_useAppLanguage": "Utilize o idioma da aplicação",
|
||||||
|
"translation_downloadedModelLabel": "Modelo baixado",
|
||||||
|
"translation_presetModelLabel": "Modelo pré-definido da Hugging Face",
|
||||||
|
"translation_manualUrlLabel": "URL do modelo manual",
|
||||||
|
"translation_downloading": "Baixando...",
|
||||||
|
"translation_downloadModel": "Baixar modelo",
|
||||||
|
"translation_working": "Trabalhando...",
|
||||||
|
"translation_stop": "Pare",
|
||||||
|
"translation_mergingChunks": "Combinando os fragmentos baixados em um único arquivo...",
|
||||||
|
"translation_downloadedModels": "Modelos baixados",
|
||||||
|
"translation_deleteModel": "Excluir modelo",
|
||||||
|
"translation_modelDownloaded": "Modelo de tradução baixado.",
|
||||||
|
"translation_downloadStopped": "Download interrompido.",
|
||||||
|
"translation_downloadFailed": "Falha na descarga: {error}",
|
||||||
|
"translation_enterUrlFirst": "Insira primeiro a URL do modelo.",
|
||||||
|
"@scanner_linuxPairingPinPrompt": {
|
||||||
|
"placeholders": {
|
||||||
|
"deviceName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@translation_translateTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"language": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_messageTranslation": "Tradução da mensagem",
|
||||||
|
"translation_translateBeforeSending": "Traduzir antes de enviar",
|
||||||
|
"translation_composerEnabledHint": "As mensagens serão traduzidas antes de serem enviadas.",
|
||||||
|
"translation_composerDisabledHint": "Envie mensagens no idioma original, conforme digitado.",
|
||||||
|
"translation_translateTo": "Traduzir para {language}",
|
||||||
|
"translation_translationOptions": "Opções de tradução",
|
||||||
|
"translation_systemLanguage": "Idioma do sistema",
|
||||||
|
"scanner_linuxPairingShowPin": "Mostrar PIN",
|
||||||
|
"scanner_linuxPairingHidePin": "Ocultar PIN",
|
||||||
|
"scanner_linuxPairingPinPrompt": "Insira o PIN para {deviceName} (deixe em branco se não houver).",
|
||||||
|
"scanner_linuxPairingPinTitle": "PIN de emparelhamento Bluetooth",
|
||||||
|
"repeater_cliQuickClockSync": "Sincronização do Relógio",
|
||||||
|
"repeater_cliQuickDiscovery": "Descobrir Vizinhos"
|
||||||
}
|
}
|
||||||
+469
-4
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"channels_channelDeleteFailed": "Не удалось удалить канал {name}.",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@@locale": "ru",
|
"@@locale": "ru",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Контакты",
|
"nav_contacts": "Контакты",
|
||||||
@@ -204,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": "Нет контактов, соответствующих фильтру",
|
||||||
@@ -226,6 +235,8 @@
|
|||||||
"channels_publicChannel": "Публичный канал",
|
"channels_publicChannel": "Публичный канал",
|
||||||
"channels_privateChannel": "Приватный канал",
|
"channels_privateChannel": "Приватный канал",
|
||||||
"channels_editChannel": "Изменить канал",
|
"channels_editChannel": "Изменить канал",
|
||||||
|
"channels_muteChannel": "Отключить уведомления канала",
|
||||||
|
"channels_unmuteChannel": "Включить уведомления канала",
|
||||||
"channels_deleteChannel": "Удалить канал",
|
"channels_deleteChannel": "Удалить канал",
|
||||||
"channels_deleteChannelConfirm": "Удалить \"{name}\"? Это действие нельзя отменить.",
|
"channels_deleteChannelConfirm": "Удалить \"{name}\"? Это действие нельзя отменить.",
|
||||||
"channels_channelDeleted": "Канал \"{name}\" удалён",
|
"channels_channelDeleted": "Канал \"{name}\" удалён",
|
||||||
@@ -467,8 +478,8 @@
|
|||||||
"repeater_telemetrySubtitle": "Просмотр телеметрии датчиков и системной статистики",
|
"repeater_telemetrySubtitle": "Просмотр телеметрии датчиков и системной статистики",
|
||||||
"repeater_cli": "CLI",
|
"repeater_cli": "CLI",
|
||||||
"repeater_cliSubtitle": "Отправка команд репитеру",
|
"repeater_cliSubtitle": "Отправка команд репитеру",
|
||||||
"repeater_neighbours": "Соседи",
|
"repeater_neighbors": "Соседи",
|
||||||
"repeater_neighboursSubtitle": "Просмотр соседей на нулевом хопе.",
|
"repeater_neighborsSubtitle": "Просмотр соседей на нулевом хопе.",
|
||||||
"repeater_settings": "Настройки",
|
"repeater_settings": "Настройки",
|
||||||
"repeater_settingsSubtitle": "Настройка параметров репитера",
|
"repeater_settingsSubtitle": "Настройка параметров репитера",
|
||||||
"repeater_statusTitle": "Статус репитера",
|
"repeater_statusTitle": "Статус репитера",
|
||||||
@@ -661,7 +672,7 @@
|
|||||||
"neighbors_receivedData": "Полученные данные о соседях",
|
"neighbors_receivedData": "Полученные данные о соседях",
|
||||||
"neighbors_requestTimedOut": "Время ожидания данных о соседях истекло.",
|
"neighbors_requestTimedOut": "Время ожидания данных о соседях истекло.",
|
||||||
"neighbors_errorLoading": "Ошибка загрузки соседей: {error}",
|
"neighbors_errorLoading": "Ошибка загрузки соседей: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Соседи репитеров",
|
"neighbors_repeatersNeighbors": "Соседи репитеров",
|
||||||
"neighbors_noData": "Данные о соседях недоступны.",
|
"neighbors_noData": "Данные о соседях недоступны.",
|
||||||
"neighbors_unknownContact": "Неизвестный {pubkey}",
|
"neighbors_unknownContact": "Неизвестный {pubkey}",
|
||||||
"neighbors_heardA ago": "Слышали: {time} назад",
|
"neighbors_heardA ago": "Слышали: {time} назад",
|
||||||
@@ -794,6 +805,8 @@
|
|||||||
"contacts_invalidAdvertFormat": "Недействительные контактные данные",
|
"contacts_invalidAdvertFormat": "Недействительные контактные данные",
|
||||||
"contacts_zeroHopAdvert": "Реклама Zero Hop",
|
"contacts_zeroHopAdvert": "Реклама Zero Hop",
|
||||||
"appSettings_languageUk": "Українська",
|
"appSettings_languageUk": "Українська",
|
||||||
|
"appSettings_enableMessageTracing": "Включить трассировку сообщений",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Показывать подробные метаданные о маршрутизации и времени для сообщений",
|
||||||
"contacts_floodAdvert": "Рекламный поток",
|
"contacts_floodAdvert": "Рекламный поток",
|
||||||
"contacts_clipboardEmpty": "Буфер обмена пуст.",
|
"contacts_clipboardEmpty": "Буфер обмена пуст.",
|
||||||
"contacts_copyAdvertToClipboard": "Копировать рекламу в буфер обмена",
|
"contacts_copyAdvertToClipboard": "Копировать рекламу в буфер обмена",
|
||||||
@@ -834,7 +847,459 @@
|
|||||||
"scanner_enableBluetooth": "Включите Bluetooth",
|
"scanner_enableBluetooth": "Включите Bluetooth",
|
||||||
"scanner_bluetoothOff": "Bluetooth выключен",
|
"scanner_bluetoothOff": "Bluetooth выключен",
|
||||||
"scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства.",
|
"scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства.",
|
||||||
|
"scanner_chromeRequired": "Требуется браузер Chrome",
|
||||||
|
"scanner_chromeRequiredMessage": "Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.",
|
||||||
|
"snrIndicator_nearByRepeaters": "Ближайшие ретрансляторы",
|
||||||
|
"snrIndicator_lastSeen": "Последний раз видели",
|
||||||
|
"chat_ShowAllPaths": "Показать все пути",
|
||||||
"settings_clientRepeatFreqWarning": "Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.",
|
"settings_clientRepeatFreqWarning": "Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.",
|
||||||
"settings_clientRepeatSubtitle": "Позвольте этому устройству повторять пакеты данных для других устройств.",
|
"settings_clientRepeatSubtitle": "Позвольте этому устройству повторять пакеты данных для других устройств.",
|
||||||
"settings_clientRepeat": "Повторение \"вне сети\""
|
"settings_clientRepeat": "Повторение \"вне сети\"",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "Данные о высоте LOS: Open-Meteo (CC BY 4.0)",
|
||||||
|
"appSettings_unitsTitle": "Единицы",
|
||||||
|
"appSettings_unitsMetric": "Метрическая (м/км)",
|
||||||
|
"appSettings_unitsImperial": "Имперская (ft / mi)",
|
||||||
|
"map_lineOfSight": "Линия видимости",
|
||||||
|
"map_losScreenTitle": "Линия видимости",
|
||||||
|
"losSelectStartEnd": "Выберите начальный и конечный узлы для LOS.",
|
||||||
|
"losRunFailed": "Проверка прямой видимости не удалась: {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Очистить все точки",
|
||||||
|
"losRunToViewElevationProfile": "Запустите LOS, чтобы просмотреть профиль высот.",
|
||||||
|
"losMenuTitle": "ЛОС Меню",
|
||||||
|
"losMenuSubtitle": "Коснитесь узлов или нажмите и удерживайте карту для выбора пользовательских точек.",
|
||||||
|
"losShowDisplayNodes": "Показать узлы отображения",
|
||||||
|
"losCustomPoints": "Пользовательские точки",
|
||||||
|
"losCustomPointLabel": "Пользовательский {index}",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Точка А",
|
||||||
|
"losPointB": "Точка Б",
|
||||||
|
"losAntennaA": "Антенна А: {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Антенна Б: {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Запустить ЛОС",
|
||||||
|
"losNoElevationData": "Нет данных о высоте",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, свободная зона видимости, минимальный зазор {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, заблокирован {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "ЛОС: проверяю...",
|
||||||
|
"losStatusNoData": "ЛОС: нет данных",
|
||||||
|
"losStatusSummary": "LOS: {clear}/{total} очищено, {blocked} заблокировано, {unknown} неизвестно.",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Данные о высоте недоступны для одного или нескольких образцов.",
|
||||||
|
"losErrorInvalidInput": "Неверные данные о точках/высоте для расчета LOS.",
|
||||||
|
"losRenameCustomPoint": "Переименовать пользовательскую точку",
|
||||||
|
"losPointName": "Имя точки",
|
||||||
|
"losShowPanelTooltip": "Показать панель LOS",
|
||||||
|
"losHidePanelTooltip": "Скрыть панель LOS",
|
||||||
|
"losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Радиогоризонт",
|
||||||
|
"losLegendLosBeam": "Линия прямой видимости",
|
||||||
|
"losLegendTerrain": "Рельеф",
|
||||||
|
"losFrequencyLabel": "Частота",
|
||||||
|
"losFrequencyInfoTooltip": "Просмотреть детали расчёта",
|
||||||
|
"losFrequencyDialogTitle": "Расчёт радиогоризонта",
|
||||||
|
"losFrequencyDialogDescription": "Начиная с k={baselineK} на частоте {baselineFreq} МГц, расчет корректирует коэффициент k для текущего диапазона {frequencyMHz} МГц, который определяет изогнутую границу радиогоризонта.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listFilter_addToFavorites": "Добавить в избранное",
|
||||||
|
"listFilter_favorites": "Избранное",
|
||||||
|
"listFilter_removeFromFavorites": "Удалить из избранного",
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_searchRepeaters": "Поиск {number}{str} ретрансляторов...",
|
||||||
|
"contacts_searchContactsNoNumber": "Поиск контактов...",
|
||||||
|
"contacts_unread": "Непрочитанное",
|
||||||
|
"contacts_searchRoomServers": "Поиск {number}{str} серверов комнат...",
|
||||||
|
"contacts_searchFavorites": "Поиск {number}{str} избранного...",
|
||||||
|
"contacts_searchUsers": "Поиск {number}{str} пользователей...",
|
||||||
|
"settings_contactSettings": "Настройки контактов",
|
||||||
|
"settings_contactSettingsSubtitle": "Настройки добавления контактов",
|
||||||
|
"contactsSettings_autoAddTitle": "Автоматическое обнаружение",
|
||||||
|
"contactsSettings_title": "Настройки контактов",
|
||||||
|
"contactsSettings_otherTitle": "Другие настройки, связанные с контактами",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Разрешить компаньону автоматически добавлять обнаруженных пользователей",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Автоматически добавлять серверы комнат",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Автоматически добавлять датчики",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Разрешить компаньону автоматически добавлять обнаруженные датчики",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Автоматически добавлять пользователей",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Перезаписать самое старое",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Автоматически добавлять ретрансляторы",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Разрешить спутнику автоматически добавлять обнаруженные ретрансляторы",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Разрешить компаньону автоматически добавлять обнаруженные сервера комнат.",
|
||||||
|
"discoveredContacts_noMatching": "Нет совпадающих контактов",
|
||||||
|
"discoveredContacts_searchHint": "Найденные контакты поиска",
|
||||||
|
"discoveredContacts_contactAdded": "Контакт добавлен",
|
||||||
|
"discoveredContacts_copyContact": "Копировать контакт в буфер обмена",
|
||||||
|
"discoveredContacts_addContact": "Добавить контакт",
|
||||||
|
"discoveredContacts_Title": "Обнаруженные контакты",
|
||||||
|
"discoveredContacts_deleteContact": "Удалить контакт",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Когда список контактов заполнен, будет заменен самый старый контакт, который не находится в избранном.",
|
||||||
|
"common_deleteAll": "Удалить все",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Вы уверены, что хотите удалить все обнаруженные контакты?",
|
||||||
|
"discoveredContacts_deleteContactAll": "Удалить Все Обнаруженные Контакты",
|
||||||
|
"map_guessedLocation": "Угаданное место",
|
||||||
|
"map_showGuessedLocations": "Отобразить предполагаемые места расположения узлов",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenSubtitle": "Выберите обнаруженное устройство с последовательным интерфейсом и подключите его напрямую к вашему узлу MeshCore.",
|
||||||
|
"usbScreenTitle": "Подключение через USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenStatus": "Выберите USB-устройство",
|
||||||
|
"usbScreenNote": "USB-серийный порт активен на поддерживаемых устройствах Android и на настольных платформах.",
|
||||||
|
"usbScreenEmptyState": "Не обнаружено устройств USB. Подключите одно из них и обновите список.",
|
||||||
|
"usbErrorPermissionDenied": "Запрос на доступ через USB был отклонен.",
|
||||||
|
"usbErrorDeviceMissing": "Выбранное USB-устройство больше недоступно.",
|
||||||
|
"usbErrorInvalidPort": "Выберите действительное USB-устройство.",
|
||||||
|
"usbErrorBusy": "Еще одно запрошенное соединение через USB уже находится в процессе.",
|
||||||
|
"usbErrorNotConnected": "Ни одно USB-устройство не подключено.",
|
||||||
|
"usbErrorOpenFailed": "Не удалось открыть выбранное USB-устройство.",
|
||||||
|
"usbErrorConnectFailed": "Не удалось установить соединение с выбранным USB-устройством.",
|
||||||
|
"usbErrorUnsupported": "Поддержка последовательного USB отсутствует на данной платформе.",
|
||||||
|
"usbErrorAlreadyActive": "USB-соединение уже установлено.",
|
||||||
|
"usbErrorNoDeviceSelected": "Не было выбрано ни одно устройство USB.",
|
||||||
|
"usbErrorPortClosed": "USB-соединение не установлено.",
|
||||||
|
"usbFallbackDeviceName": "Устройство для последовательного подключения к сети",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_searching": "Поиск USB-устройств...",
|
||||||
|
"usbStatus_connecting": "Подключение к USB-устройству...",
|
||||||
|
"usbConnectionFailed": "Не удалось установить соединение через USB: {error}",
|
||||||
|
"usbStatus_notConnected": "Выберите USB-устройство",
|
||||||
|
"usbErrorConnectTimedOut": "Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion.",
|
||||||
|
"@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": "Вернуться обратно по тому же пути",
|
||||||
|
"@radioStats_noiseFloor": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_lastRssi": {
|
||||||
|
"placeholders": {
|
||||||
|
"rssiDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_lastSnr": {
|
||||||
|
"placeholders": {
|
||||||
|
"snr": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_txAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_rxAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_stripNoise": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chat_sendCooldown": "Пожалуйста, подождите немного, прежде чем отправлять сообщение снова.",
|
||||||
|
"appSettings_jumpToOldestUnread": "Перейти к самому старому непрочитанному сообщению",
|
||||||
|
"appSettings_languageHu": "Венгерский",
|
||||||
|
"appSettings_jumpToOldestUnreadSubtitle": "При открытии чата с непрочитанными сообщениями, прокрутите страницу, чтобы увидеть первое непрочитанное сообщение, а не последнее.",
|
||||||
|
"appSettings_languageJa": "Японский",
|
||||||
|
"appSettings_languageKo": "Корейский",
|
||||||
|
"radioStats_tooltip": "Статистика радио и беспроводной сети",
|
||||||
|
"radioStats_screenTitle": "Статистика радиовещания",
|
||||||
|
"radioStats_notConnected": "Подключитесь к устройству, чтобы просмотреть статистику радио.",
|
||||||
|
"radioStats_firmwareTooOld": "Для работы радиостатистики требуется установленная версия прошивки v8 или более новая.",
|
||||||
|
"radioStats_waiting": "Ожидаем данных…",
|
||||||
|
"radioStats_noiseFloor": "Уровень шума: {noiseDbm} дБм",
|
||||||
|
"radioStats_lastRssi": "Последнее значение RSSI: {rssiDbm} дБм",
|
||||||
|
"radioStats_lastSnr": "Последнее значение SNR: {snr} дБ",
|
||||||
|
"radioStats_txAir": "Время эфира на телеканале TX (общее): {seconds} секунд",
|
||||||
|
"radioStats_rxAir": "Общее время использования RX (в секундах): {seconds} с",
|
||||||
|
"radioStats_chartCaption": "Уровень шума (дБм) на основе последних измерений.",
|
||||||
|
"radioStats_stripNoise": "Уровень шума: {noiseDbm} дБм",
|
||||||
|
"radioStats_stripWaiting": "Получение данных о радио…",
|
||||||
|
"radioStats_settingsTile": "Статистика радиовещания",
|
||||||
|
"radioStats_settingsSubtitle": "Уровень шума, RSSI, SNR и время передачи",
|
||||||
|
"@translation_downloadFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_enableSubtitle": "Переводить входящие сообщения и позволять предварительный перевод перед отправкой.",
|
||||||
|
"translation_composerTitle": "Переводить перед отправкой",
|
||||||
|
"translation_title": "Перевод",
|
||||||
|
"translation_enableTitle": "Включить перевод",
|
||||||
|
"translation_composerSubtitle": "Управляет исходным состоянием значка перевода, предоставляемого редактором.",
|
||||||
|
"translation_targetLanguage": "Целевой язык",
|
||||||
|
"translation_useAppLanguage": "Используйте язык приложения",
|
||||||
|
"translation_downloadedModelLabel": "Загруженная модель",
|
||||||
|
"translation_presetModelLabel": "Предопределенная модель от Hugging Face",
|
||||||
|
"translation_manualUrlLabel": "Ссылка на руководство",
|
||||||
|
"translation_downloadModel": "Скачать модель",
|
||||||
|
"translation_downloading": "Загрузка...",
|
||||||
|
"translation_stop": "Прекратите",
|
||||||
|
"translation_working": "Работа...",
|
||||||
|
"translation_mergingChunks": "Объединение скачанных фрагментов в один финальный файл...",
|
||||||
|
"translation_downloadedModels": "Загруженные модели",
|
||||||
|
"translation_deleteModel": "Удалить модель",
|
||||||
|
"translation_modelDownloaded": "Модель перевода загружена.",
|
||||||
|
"translation_downloadStopped": "Процесс загрузки был прерван.",
|
||||||
|
"translation_downloadFailed": "Не удалось скачать: {error}",
|
||||||
|
"translation_enterUrlFirst": "Сначала введите URL модели.",
|
||||||
|
"@scanner_linuxPairingPinPrompt": {
|
||||||
|
"placeholders": {
|
||||||
|
"deviceName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@translation_translateTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"language": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_translateBeforeSending": "Перевести перед отправкой",
|
||||||
|
"translation_composerEnabledHint": "Сообщения будут переведены перед отправкой.",
|
||||||
|
"translation_messageTranslation": "Перевод сообщения",
|
||||||
|
"translation_composerDisabledHint": "Отправляйте сообщения на языке, в котором они были изначально набраны.",
|
||||||
|
"translation_translateTo": "Перевести на {language}",
|
||||||
|
"translation_translationOptions": "Варианты перевода",
|
||||||
|
"translation_systemLanguage": "Язык системы",
|
||||||
|
"scanner_linuxPairingShowPin": "Показать PIN",
|
||||||
|
"scanner_linuxPairingPinPrompt": "Введите PIN‑код для {deviceName} (оставьте пустым, если нет).",
|
||||||
|
"scanner_linuxPairingHidePin": "Скрыть PIN",
|
||||||
|
"scanner_linuxPairingPinTitle": "PIN‑код сопряжения Bluetooth",
|
||||||
|
"repeater_cliQuickDiscovery": "Обнаружить Соседей",
|
||||||
|
"repeater_cliQuickClockSync": "Синхронизация часов"
|
||||||
}
|
}
|
||||||
+469
-4
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"channels_channelDeleteFailed": "Kanál \"{name}\" sa nepodarilo odstrániť",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@@locale": "sk",
|
"@@locale": "sk",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Kontakty",
|
"nav_contacts": "Kontakty",
|
||||||
@@ -277,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": {
|
||||||
@@ -334,6 +343,8 @@
|
|||||||
"channels_publicChannel": "Veľké verejne kanály",
|
"channels_publicChannel": "Veľké verejne kanály",
|
||||||
"channels_privateChannel": "Osobné kanál",
|
"channels_privateChannel": "Osobné kanál",
|
||||||
"channels_editChannel": "Upraviť kanál",
|
"channels_editChannel": "Upraviť kanál",
|
||||||
|
"channels_muteChannel": "Stlmiť kanál",
|
||||||
|
"channels_unmuteChannel": "Zrušiť stlmenie kanála",
|
||||||
"channels_deleteChannel": "Odstrániť kanál",
|
"channels_deleteChannel": "Odstrániť kanál",
|
||||||
"channels_deleteChannelConfirm": "Odstrániť \"{name}\"? To sa nedá zrušiť.",
|
"channels_deleteChannelConfirm": "Odstrániť \"{name}\"? To sa nedá zrušiť.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
@@ -1351,12 +1362,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighboursSubtitle": "Zobraziť susedné body bez skokov.",
|
"repeater_neighborsSubtitle": "Zobraziť susedné body bez skokov.",
|
||||||
"neighbors_requestTimedOut": "Súďia žiadajú o časové ukončenie.",
|
"neighbors_requestTimedOut": "Súďia žiadajú o časové ukončenie.",
|
||||||
"neighbors_receivedData": "Obdielo dáta suseda",
|
"neighbors_receivedData": "Obdielo dáta suseda",
|
||||||
"repeater_neighbours": "Súsezný",
|
"repeater_neighbors": "Súsezný",
|
||||||
"neighbors_errorLoading": "Chyba pri načítaní susedov: {error}",
|
"neighbors_errorLoading": "Chyba pri načítaní susedov: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Opakovadlá Súsezná",
|
"neighbors_repeatersNeighbors": "Opakovadlá Súsezná",
|
||||||
"neighbors_noData": "Nie je dostupná žiadna informácia o susedoch.",
|
"neighbors_noData": "Nie je dostupná žiadna informácia o susedoch.",
|
||||||
"channels_createPrivateChannel": "Vytvorte súkromný kanál",
|
"channels_createPrivateChannel": "Vytvorte súkromný kanál",
|
||||||
"channels_joinPrivateChannel": "Pripojiť sa k súkromnému kanálu",
|
"channels_joinPrivateChannel": "Pripojiť sa k súkromnému kanálu",
|
||||||
@@ -1556,6 +1567,8 @@
|
|||||||
"contacts_copyAdvertToClipboard": "Kopírovať reklamu do schránky",
|
"contacts_copyAdvertToClipboard": "Kopírovať reklamu do schránky",
|
||||||
"contacts_invalidAdvertFormat": "Neplatné kontaktné údaje",
|
"contacts_invalidAdvertFormat": "Neplatné kontaktné údaje",
|
||||||
"appSettings_languageRu": "Ruština",
|
"appSettings_languageRu": "Ruština",
|
||||||
|
"appSettings_enableMessageTracing": "Povoliť sledovanie správ",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Zobraziť podrobné metadáta o smerovaní a časovaní správ",
|
||||||
"contacts_addContactFromClipboard": "Pridať kontakt z schránky",
|
"contacts_addContactFromClipboard": "Pridať kontakt z schránky",
|
||||||
"contacts_contactImported": "Kontakt bol importovaný.",
|
"contacts_contactImported": "Kontakt bol importovaný.",
|
||||||
"contacts_zeroHopContactAdvertSent": "Poslal kontakt cez inzerát.",
|
"contacts_zeroHopContactAdvertSent": "Poslal kontakt cez inzerát.",
|
||||||
@@ -1592,9 +1605,461 @@
|
|||||||
"map_runTrace": "Spustiť trasovaním cesty",
|
"map_runTrace": "Spustiť trasovaním cesty",
|
||||||
"map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené.",
|
"map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené.",
|
||||||
"scanner_bluetoothOffMessage": "Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.",
|
"scanner_bluetoothOffMessage": "Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.",
|
||||||
|
"scanner_chromeRequired": "Vyžaduje sa prehliadač Chrome",
|
||||||
|
"scanner_chromeRequiredMessage": "Táto webová aplikácia vyžaduje Google Chrome alebo prehliadač založený na Chromium pre podporu Bluetooth.",
|
||||||
"scanner_bluetoothOff": "Bluetooth je vypnutý",
|
"scanner_bluetoothOff": "Bluetooth je vypnutý",
|
||||||
"scanner_enableBluetooth": "Povolte Bluetooth",
|
"scanner_enableBluetooth": "Povolte Bluetooth",
|
||||||
|
"snrIndicator_lastSeen": "Naposledy videný",
|
||||||
|
"snrIndicator_nearByRepeaters": "Miestne opakovače",
|
||||||
|
"chat_ShowAllPaths": "Zobraziť všetky cesty",
|
||||||
"settings_clientRepeat": "Opätovné použitie bez elektrickej siete",
|
"settings_clientRepeat": "Opätovné použitie bez elektrickej siete",
|
||||||
"settings_clientRepeatFreqWarning": "Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.",
|
"settings_clientRepeatFreqWarning": "Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.",
|
||||||
"settings_clientRepeatSubtitle": "Umožnite, aby toto zariadenie opakovávalo siete pre ostatných."
|
"settings_clientRepeatSubtitle": "Umožnite, aby toto zariadenie opakovávalo siete pre ostatných.",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "Údaje o nadmorskej výške LOS: Open-Meteo (CC BY 4.0)",
|
||||||
|
"appSettings_unitsTitle": "Jednotky",
|
||||||
|
"appSettings_unitsMetric": "Metrické (m / km)",
|
||||||
|
"appSettings_unitsImperial": "Imperiálne (ft / mi)",
|
||||||
|
"map_lineOfSight": "Line of Sight",
|
||||||
|
"map_losScreenTitle": "Line of Sight",
|
||||||
|
"losSelectStartEnd": "Vyberte počiatočný a koncový uzol pre LOS.",
|
||||||
|
"losRunFailed": "Kontrola priamej viditeľnosti zlyhala: {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Vymazať všetky body",
|
||||||
|
"losRunToViewElevationProfile": "Ak chcete zobraziť výškový profil, spustite LOS",
|
||||||
|
"losMenuTitle": "Menu LOS",
|
||||||
|
"losMenuSubtitle": "Klepnutím na uzly alebo dlhým stlačením mapy získate vlastné body",
|
||||||
|
"losShowDisplayNodes": "Zobraziť uzly zobrazenia",
|
||||||
|
"losCustomPoints": "Vlastné body",
|
||||||
|
"losCustomPointLabel": "Vlastné {index}",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Bod A",
|
||||||
|
"losPointB": "Bod B",
|
||||||
|
"losAntennaA": "Anténa A: {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Anténa B: {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Spustite LOS",
|
||||||
|
"losNoElevationData": "Žiadne údaje o nadmorskej výške",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, vymazať LOS, min. vôľa {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, blokovaný {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "LOS: kontrolujem...",
|
||||||
|
"losStatusNoData": "LOS: žiadne údaje",
|
||||||
|
"losStatusSummary": "LOS: {clear}/{total} vymazané, {blocked} blokované, {unknown} neznáme",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Údaje o nadmorskej výške nie sú k dispozícii pre jednu alebo viacero vzoriek.",
|
||||||
|
"losErrorInvalidInput": "Neplatné body/údaje o nadmorskej výške pre výpočet LOS.",
|
||||||
|
"losRenameCustomPoint": "Premenovať vlastný bod",
|
||||||
|
"losPointName": "Názov bodu",
|
||||||
|
"losShowPanelTooltip": "Zobraziť panel LOS",
|
||||||
|
"losHidePanelTooltip": "Skryť panel LOS",
|
||||||
|
"losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Rádiový horizont",
|
||||||
|
"losLegendLosBeam": "Priama viditeľnosť",
|
||||||
|
"losLegendTerrain": "Terén",
|
||||||
|
"losFrequencyLabel": "Frekvencia",
|
||||||
|
"losFrequencyInfoTooltip": "Zobraziť podrobnosti výpočtu",
|
||||||
|
"losFrequencyDialogTitle": "Výpočet rádiového horizontu",
|
||||||
|
"losFrequencyDialogDescription": "Počnúc od k={baselineK} pri {baselineFreq} MHz výpočet upraví k-faktor pre aktuálne pásmo {frequencyMHz} MHz, ktorý definuje zakrivený strop rádiového horizontu.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listFilter_removeFromFavorites": "Odstrániť z označení",
|
||||||
|
"listFilter_addToFavorites": "Pridaj do obľúbených",
|
||||||
|
"listFilter_favorites": "Obľúbené",
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_searchRoomServers": "Hľadaj {number}{str} serverov miestností...",
|
||||||
|
"contacts_searchFavorites": "Hľadať {number}{str} obľúbené...",
|
||||||
|
"contacts_searchRepeaters": "Hľadať {number}{str} opakovače...",
|
||||||
|
"contacts_searchUsers": "Hľadať {number}{str} používateľov...",
|
||||||
|
"contacts_searchContactsNoNumber": "Hľadať kontakty...",
|
||||||
|
"contacts_unread": "Neprečítané",
|
||||||
|
"settings_contactSettingsSubtitle": "Nastavenia pre pridávanie kontaktov.",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Automaticky pridávať užívateľov",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Povoliť spoločníkovi automaticky pridávať objavených užívateľov.",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Automaticky pridávať opakovače",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Automaticky pridávať server miestnosti",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Povoliť spoločníkovi automaticky pridať objavené serverové miestnosti.",
|
||||||
|
"contactsSettings_autoAddTitle": "Automatické zisťovanie",
|
||||||
|
"contactsSettings_title": "Nastavenia kontaktov",
|
||||||
|
"contactsSettings_otherTitle": "Ďalšie nastavenia súvisiace s kontaktami",
|
||||||
|
"settings_contactSettings": "Nastavenia kontaktov",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Automaticky pridávať senzory",
|
||||||
|
"discoveredContacts_noMatching": "Žiadne zhodné kontakty",
|
||||||
|
"discoveredContacts_searchHint": "Vyhľadať objavené kontakty",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Povoliť spoločníkovi automaticky pridávať objavené repeater.",
|
||||||
|
"discoveredContacts_contactAdded": "Kontakt bol pridaný",
|
||||||
|
"discoveredContacts_copyContact": "Kopírovať kontakt do schránky",
|
||||||
|
"discoveredContacts_deleteContact": "Zmazať kontakt",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Povoliť spoločníkovi automaticky pridávať objavené senzory.",
|
||||||
|
"discoveredContacts_Title": "Objavené kontakty",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Prepísať najstaršie",
|
||||||
|
"discoveredContacts_addContact": "Pridať kontakt",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Keď je zoznam kontaktov plný, bude nahradený najstarší neoznačený kontakt.",
|
||||||
|
"discoveredContacts_deleteContactAll": "Zmazať všetky objavené kontakty",
|
||||||
|
"common_deleteAll": "Zmazať všetko",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Ste si istí, že chcete zmazať všetky objavené kontakty?",
|
||||||
|
"map_showGuessedLocations": "Zobraziť umiestnenia odhadnutých uzlov",
|
||||||
|
"map_guessedLocation": "Odhadnutá lokalita",
|
||||||
|
"usbScreenTitle": "Pripojte cez USB",
|
||||||
|
"usbScreenSubtitle": "Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vašej MeshCore uzlu.",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"usbScreenStatus": "Vyberte USB zariadenie",
|
||||||
|
"usbScreenNote": "USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.",
|
||||||
|
"usbScreenEmptyState": "Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte.",
|
||||||
|
"usbErrorPermissionDenied": "Žiadosť o prístup cez USB bola zamietnutá.",
|
||||||
|
"usbErrorDeviceMissing": "Vybrané USB zariadenie už nie je dostupné.",
|
||||||
|
"usbErrorInvalidPort": "Vyberte platné USB zariadenie.",
|
||||||
|
"usbErrorBusy": "Ďalšia požiadavka na pripojenie cez USB je aktuálne v procese.",
|
||||||
|
"usbErrorNotConnected": "Nie je pripojené žiadne USB zariadenie.",
|
||||||
|
"usbErrorOpenFailed": "Nepodarilo sa otvoriť vybrané USB zariadenie.",
|
||||||
|
"usbErrorConnectFailed": "Nepodarilo sa sa sa pripojiť k vybranému USB zariadeniu.",
|
||||||
|
"usbErrorUnsupported": "Podpora USB sériového rozhrania nie je na tejto platforme dostupná.",
|
||||||
|
"usbErrorAlreadyActive": "Pripojenie cez USB je už aktivované.",
|
||||||
|
"usbErrorNoDeviceSelected": "Nebolo vybrané žiadne USB zariadenie.",
|
||||||
|
"usbErrorPortClosed": "Pripojenie cez USB nie je aktivované.",
|
||||||
|
"usbFallbackDeviceName": "Webový sériový zariadenie",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_searching": "Hľadanie USB zariadení...",
|
||||||
|
"usbConnectionFailed": "Neúspešné pripojenie cez USB: {error}",
|
||||||
|
"usbStatus_notConnected": "Vyberte USB zariadenie",
|
||||||
|
"usbStatus_connecting": "Pripojenie k USB zariadeniu...",
|
||||||
|
"usbErrorConnectTimedOut": "Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion.",
|
||||||
|
"@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.",
|
||||||
|
"@radioStats_noiseFloor": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_lastRssi": {
|
||||||
|
"placeholders": {
|
||||||
|
"rssiDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_lastSnr": {
|
||||||
|
"placeholders": {
|
||||||
|
"snr": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_txAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_rxAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_stripNoise": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chat_sendCooldown": "Prosím, počkajte chvíľu, než zašlete znova.",
|
||||||
|
"appSettings_jumpToOldestUnread": "Presk oceň",
|
||||||
|
"appSettings_jumpToOldestUnreadSubtitle": "Pri otvorení chatu s neprečítanými správami, prejdite do prvého neprečítaného, namiesto poslednej.",
|
||||||
|
"appSettings_languageHu": "Maďarský",
|
||||||
|
"appSettings_languageJa": "Japonský",
|
||||||
|
"appSettings_languageKo": "Kórejský",
|
||||||
|
"radioStats_tooltip": "Statistiky rádiových a sieťových kanálov",
|
||||||
|
"radioStats_screenTitle": "Štatistiky rádiových vysielaní",
|
||||||
|
"radioStats_notConnected": "Pripojte sa k zariadeniu, aby ste mohli sledovať štatistiky rádiového vysielania.",
|
||||||
|
"radioStats_firmwareTooOld": "Statistické údaje z rádia vyžadujú sprievodný softvér verzie v8 alebo novšej.",
|
||||||
|
"radioStats_waiting": "Čakám na údaje…",
|
||||||
|
"radioStats_noiseFloor": "Úroveň hluku: {noiseDbm} dBm",
|
||||||
|
"radioStats_lastRssi": "Posledný údaj RSSI: {rssiDbm} dBm",
|
||||||
|
"radioStats_lastSnr": "Posledná hodnota SNR: {snr} dB",
|
||||||
|
"radioStats_txAir": "Čas vysielania na TX (celkový): {seconds} s",
|
||||||
|
"radioStats_rxAir": "Čas RX (celkový): {seconds} s",
|
||||||
|
"radioStats_chartCaption": "Úroveň šumu (dBm) pre posledné vzorky.",
|
||||||
|
"radioStats_stripNoise": "Úroveň hluku: {noiseDbm} dBm",
|
||||||
|
"radioStats_stripWaiting": "Získavanie údajov o rádiu…",
|
||||||
|
"radioStats_settingsTile": "Štatistiky rádiových vysielaní",
|
||||||
|
"radioStats_settingsSubtitle": "Úroveň hluku, RSSI, SNR a časové rozloženie",
|
||||||
|
"@translation_downloadFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_enableSubtitle": "Prekladajte prichádzajúce správy a umožnite ich preklad pred odoslaním.",
|
||||||
|
"translation_enableTitle": "Aktivovať preklad",
|
||||||
|
"translation_composerTitle": "Preložte pred odeslaním",
|
||||||
|
"translation_title": "Preklad",
|
||||||
|
"translation_composerSubtitle": "Riadi výchoce stav ikony pre preklad, ktorú používa program.",
|
||||||
|
"translation_targetLanguage": "Cieľový jazyk",
|
||||||
|
"translation_useAppLanguage": "Použite jazyk aplikácie",
|
||||||
|
"translation_downloadedModelLabel": "Stiahnutý model",
|
||||||
|
"translation_presetModelLabel": "Prednastavený model od Hugging Face",
|
||||||
|
"translation_manualUrlLabel": "Odkaz na manuál (v elektronickej forme)",
|
||||||
|
"translation_downloadModel": "Stiahnuť model",
|
||||||
|
"translation_downloading": "Stiahnutie...",
|
||||||
|
"translation_working": "Práca...",
|
||||||
|
"translation_stop": "Zastavte",
|
||||||
|
"translation_mergingChunks": "Sliečenie stiahnutých častí do konečného súboru...",
|
||||||
|
"translation_downloadedModels": "Stiahnuté modely",
|
||||||
|
"translation_deleteModel": "Odstrániť model",
|
||||||
|
"translation_modelDownloaded": "Model pre preklad bol stiahnutý.",
|
||||||
|
"translation_downloadStopped": "Stiahnutie bolo prerušené.",
|
||||||
|
"translation_downloadFailed": "Neúspešné stiahnutie: {error}",
|
||||||
|
"translation_enterUrlFirst": "Najprv zadajte URL pre konkrétny model.",
|
||||||
|
"@scanner_linuxPairingPinPrompt": {
|
||||||
|
"placeholders": {
|
||||||
|
"deviceName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scanner_linuxPairingHidePin": "Skryť PIN",
|
||||||
|
"scanner_linuxPairingShowPin": "Zobraziť PIN",
|
||||||
|
"scanner_linuxPairingPinTitle": "PIN pre párovanie cez Bluetooth",
|
||||||
|
"scanner_linuxPairingPinPrompt": "Zadajte PIN pre {deviceName} (ak neexistuje, nechajte prázdne).",
|
||||||
|
"@translation_translateTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"language": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_composerDisabledHint": "Posielajte správy v pôvodnej písanom jazyku.",
|
||||||
|
"translation_composerEnabledHint": "Správy budú preložené, než budú odoslané.",
|
||||||
|
"translation_translateBeforeSending": "Preložte pred odeslaním",
|
||||||
|
"translation_messageTranslation": "Preklad textu",
|
||||||
|
"translation_translateTo": "Preložte do {language}",
|
||||||
|
"translation_translationOptions": "Možnosti prekladania",
|
||||||
|
"translation_systemLanguage": "Jazyk systému",
|
||||||
|
"repeater_cliQuickClockSync": "Synchronizácia hodin",
|
||||||
|
"repeater_cliQuickDiscovery": "Objaviť susedov"
|
||||||
}
|
}
|
||||||
+469
-4
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"channels_channelDeleteFailed": "Kanala {name} ni bilo mogoče izbrisati",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@@locale": "sl",
|
"@@locale": "sl",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Stiki",
|
"nav_contacts": "Stiki",
|
||||||
@@ -277,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": {
|
||||||
@@ -334,6 +343,8 @@
|
|||||||
"channels_publicChannel": "Javni kanal",
|
"channels_publicChannel": "Javni kanal",
|
||||||
"channels_privateChannel": "Zasebni kanal",
|
"channels_privateChannel": "Zasebni kanal",
|
||||||
"channels_editChannel": "Uredi kanal",
|
"channels_editChannel": "Uredi kanal",
|
||||||
|
"channels_muteChannel": "Utišaj kanal",
|
||||||
|
"channels_unmuteChannel": "Vklopi obvestila kanala",
|
||||||
"channels_deleteChannel": "Pošlji kanal",
|
"channels_deleteChannel": "Pošlji kanal",
|
||||||
"channels_deleteChannelConfirm": "Izbrišem \"{name}\"? To se ne da povrniti.",
|
"channels_deleteChannelConfirm": "Izbrišem \"{name}\"? To se ne da povrniti.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
@@ -1351,12 +1362,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighboursSubtitle": "Pogledati nič sosednjih hopjev.",
|
"repeater_neighborsSubtitle": "Pogledati nič sosednjih hopjev.",
|
||||||
"repeater_neighbours": "Sosedi",
|
"repeater_neighbors": "Sosedi",
|
||||||
"neighbors_receivedData": "Prejeto podatke o sosedih",
|
"neighbors_receivedData": "Prejeto podatke o sosedih",
|
||||||
"neighbors_requestTimedOut": "Sosedi zahtevajo izklop po dogovoru.",
|
"neighbors_requestTimedOut": "Sosedi zahtevajo izklop po dogovoru.",
|
||||||
"neighbors_errorLoading": "Napaka pri obnašanju sosedov: {error}",
|
"neighbors_errorLoading": "Napaka pri obnašanju sosedov: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Ponovitve Sosedi",
|
"neighbors_repeatersNeighbors": "Ponovitve Sosedi",
|
||||||
"neighbors_noData": "Niso na voljo podatki o sosedih.",
|
"neighbors_noData": "Niso na voljo podatki o sosedih.",
|
||||||
"channels_joinPrivateChannel": "Pridružite se zasebni skupini",
|
"channels_joinPrivateChannel": "Pridružite se zasebni skupini",
|
||||||
"channels_createPrivateChannelDesc": "Varno zaklenjeno s skrivnim ključem.",
|
"channels_createPrivateChannelDesc": "Varno zaklenjeno s skrivnim ključem.",
|
||||||
@@ -1550,6 +1561,8 @@
|
|||||||
"contacts_pathTraceTo": "Trace route to {name}",
|
"contacts_pathTraceTo": "Trace route to {name}",
|
||||||
"appSettings_languageRu": "Ruščina",
|
"appSettings_languageRu": "Ruščina",
|
||||||
"appSettings_languageUk": "Ukrajinsko",
|
"appSettings_languageUk": "Ukrajinsko",
|
||||||
|
"appSettings_enableMessageTracing": "Omogoči sledenje sporočilom",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil",
|
||||||
"contacts_contactImported": "Kontakt je bil uvožen.",
|
"contacts_contactImported": "Kontakt je bil uvožen.",
|
||||||
"contacts_contactImportFailed": "Kontakt ni bil uspešno uvožen.",
|
"contacts_contactImportFailed": "Kontakt ni bil uspešno uvožen.",
|
||||||
"contacts_zeroHopAdvert": "Reklama brez posrednikov",
|
"contacts_zeroHopAdvert": "Reklama brez posrednikov",
|
||||||
@@ -1593,8 +1606,460 @@
|
|||||||
"map_pathTraceCancelled": "Spremljanje poti je prekinjeno.",
|
"map_pathTraceCancelled": "Spremljanje poti je prekinjeno.",
|
||||||
"scanner_enableBluetooth": "Omogočite Bluetooth",
|
"scanner_enableBluetooth": "Omogočite Bluetooth",
|
||||||
"scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.",
|
"scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.",
|
||||||
|
"scanner_chromeRequired": "Zahtevan brskalnik Chrome",
|
||||||
|
"scanner_chromeRequiredMessage": "Ta spletna aplikacija za podporo Bluetooth zahteva Google Chrome ali brskalnik na osnovi Chromiuma.",
|
||||||
"scanner_bluetoothOff": "Bluetooth je izklopljen",
|
"scanner_bluetoothOff": "Bluetooth je izklopljen",
|
||||||
|
"snrIndicator_lastSeen": "Zadnjič videno",
|
||||||
|
"snrIndicator_nearByRepeaters": "Bližnji ponovitelji",
|
||||||
|
"chat_ShowAllPaths": "Prikaži vse poti",
|
||||||
"settings_clientRepeatFreqWarning": "Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.",
|
"settings_clientRepeatFreqWarning": "Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.",
|
||||||
"settings_clientRepeatSubtitle": "Omogočite temu naprave, da ponavlja paketne sporočila za druge.",
|
"settings_clientRepeatSubtitle": "Omogočite temu naprave, da ponavlja paketne sporočila za druge.",
|
||||||
"settings_clientRepeat": "Neovadno ponavljanje"
|
"settings_clientRepeat": "Neovadno ponavljanje",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "Podatki o višini LOS: Open-Meteo (CC BY 4.0)",
|
||||||
|
"appSettings_unitsTitle": "Enote",
|
||||||
|
"appSettings_unitsMetric": "Metrična (m/km)",
|
||||||
|
"appSettings_unitsImperial": "Imperialno (ft / mi)",
|
||||||
|
"map_lineOfSight": "Linija vida",
|
||||||
|
"map_losScreenTitle": "Linija vida",
|
||||||
|
"losSelectStartEnd": "Izberite začetno in končno vozlišče za LOS.",
|
||||||
|
"losRunFailed": "Preverjanje vidnega polja ni uspelo: {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Počisti vse točke",
|
||||||
|
"losRunToViewElevationProfile": "Zaženite LOS za ogled višinskega profila",
|
||||||
|
"losMenuTitle": "LOS meni",
|
||||||
|
"losMenuSubtitle": "Tapnite vozlišča ali dolgo pritisnite na zemljevid za točke po meri",
|
||||||
|
"losShowDisplayNodes": "Pokaži prikazna vozlišča",
|
||||||
|
"losCustomPoints": "Točke po meri",
|
||||||
|
"losCustomPointLabel": "Po meri {index}",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Točka A",
|
||||||
|
"losPointB": "Točka B",
|
||||||
|
"losAntennaA": "Antena A: {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Antena B: {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Zaženi LOS",
|
||||||
|
"losNoElevationData": "Ni podatkov o višini",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, čisti LOS, najmanjša razdalja {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, blokiral {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "LOS: preverjam ...",
|
||||||
|
"losStatusNoData": "LOS: ni podatkov",
|
||||||
|
"losStatusSummary": "LOS: {clear}/{total} jasno, {blocked} blokirano, {unknown} neznano",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Podatki o nadmorski višini niso na voljo za enega ali več vzorcev.",
|
||||||
|
"losErrorInvalidInput": "Neveljavni podatki o točkah/višini za izračun LOS.",
|
||||||
|
"losRenameCustomPoint": "Preimenujte točko po meri",
|
||||||
|
"losPointName": "Ime točke",
|
||||||
|
"losShowPanelTooltip": "Pokaži ploščo LOS",
|
||||||
|
"losHidePanelTooltip": "Skrij ploščo LOS",
|
||||||
|
"losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Radijski horizont",
|
||||||
|
"losLegendLosBeam": "Linija vidnosti",
|
||||||
|
"losLegendTerrain": "Teren",
|
||||||
|
"losFrequencyLabel": "Frekvenca",
|
||||||
|
"losFrequencyInfoTooltip": "Prikaži podrobnosti izračuna",
|
||||||
|
"losFrequencyDialogTitle": "Izračun radijskega horizonta",
|
||||||
|
"losFrequencyDialogDescription": "Začenši od k={baselineK} pri {baselineFreq} MHz, izračun prilagodi k-faktor za trenutni pas {frequencyMHz} MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listFilter_favorites": "Priljubljene",
|
||||||
|
"listFilter_removeFromFavorites": "Odstrani iz priljubljenih",
|
||||||
|
"listFilter_addToFavorites": "Dodaj v priljubljene",
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_unread": "Neprebrano",
|
||||||
|
"contacts_searchFavorites": "Iskanje {number}{str} priljubljenih...",
|
||||||
|
"contacts_searchRoomServers": "Išči {number}{str} strežnikov sob...",
|
||||||
|
"contacts_searchContactsNoNumber": "Iskanje stikov...",
|
||||||
|
"contacts_searchRepeaters": "Išči {number}{str} ponavljalnike...",
|
||||||
|
"contacts_searchUsers": "Išči {number}{str} uporabnikov...",
|
||||||
|
"settings_contactSettings": "Nastavitve stika",
|
||||||
|
"contactsSettings_autoAddTitle": "Avtomatsko odkrivanje",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Avtomatsko dodaj uporabnike",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Avtomatsko dodaj ponovitelje",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite ponovitelje.",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Avtomatsko dodaj strežnike sob",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite strežnike sob.",
|
||||||
|
"contactsSettings_otherTitle": "Druge nastavitve v zvezi s stiki",
|
||||||
|
"settings_contactSettingsSubtitle": "Nastavitve za dodajanje stikov.",
|
||||||
|
"contactsSettings_title": "Nastavitve stikov",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Avtomatsko dodaj senzorje",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite uporabnike.",
|
||||||
|
"discoveredContacts_noMatching": "Ni ujemajočih stikov",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite senzorje.",
|
||||||
|
"discoveredContacts_addContact": "Dodaj stik",
|
||||||
|
"discoveredContacts_contactAdded": "Kontakt dodan",
|
||||||
|
"discoveredContacts_copyContact": "Kopiraj stik v odložišče",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Prepiši najstarejše",
|
||||||
|
"discoveredContacts_Title": "Odkriti stiki",
|
||||||
|
"discoveredContacts_searchHint": "Najdeni stiki po iskanju",
|
||||||
|
"discoveredContacts_deleteContact": "Izbriši stik",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Ko je seznam stikov poln, bo najstarejši nestarševski stik zamenjan.",
|
||||||
|
"common_deleteAll": "Izbriši vse",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Ste prepričani, da želite izbrisati vse odkrite kontakte?",
|
||||||
|
"discoveredContacts_deleteContactAll": "Izbriši vse odkrite kontakte",
|
||||||
|
"map_guessedLocation": "Predpostavljena lokacija",
|
||||||
|
"map_showGuessedLocations": "Pokaži lokacije domnevnih not.",
|
||||||
|
"usbScreenTitle": "Povežite preko USB",
|
||||||
|
"usbScreenSubtitle": "Izberite zaznano serijsko napravo in se neposredno povežite z vašo MeshCore napravo.",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenStatus": "Izberite USB naprave",
|
||||||
|
"usbScreenNote": "USB serijska povezava je aktivna na podprtih napravah Android in na desktop platformah.",
|
||||||
|
"usbScreenEmptyState": "Niti en USB naprave niso najdeni. Povežite eno in posodobite.",
|
||||||
|
"usbErrorPermissionDenied": "Dovoljenje za dostop preko USB-ja je bilo zavrnjeno.",
|
||||||
|
"usbErrorDeviceMissing": "Izbrani USB napravej je več ne.",
|
||||||
|
"usbErrorInvalidPort": "Izberite veljavno USB naprave.",
|
||||||
|
"usbErrorBusy": "Že je v teku zahteva za povezavo preko USB.",
|
||||||
|
"usbErrorNotConnected": "Ni priklopljenih USB naprave.",
|
||||||
|
"usbErrorOpenFailed": "Uspešno ni bilo mogo, da se odpre izbran naprave USB.",
|
||||||
|
"usbErrorConnectFailed": "Niso bilo mogoče uskladiti povezave z izbranim USB napom.",
|
||||||
|
"usbErrorUnsupported": "USB serijska komunikacija ni podprta na tej platformi.",
|
||||||
|
"usbErrorAlreadyActive": "USB povezava je že aktivirana.",
|
||||||
|
"usbErrorNoDeviceSelected": "Ni bilo izbranega USB naprave.",
|
||||||
|
"usbErrorPortClosed": "USB povezava ni aktivirana.",
|
||||||
|
"usbFallbackDeviceName": "Naprave za serijsko komunikacijo preko spleta",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_notConnected": "Izberite USB naprave.",
|
||||||
|
"usbStatus_connecting": "Povezava z USB napravo...",
|
||||||
|
"usbStatus_searching": "Iskanje USB naprav...",
|
||||||
|
"usbConnectionFailed": "Napaka pri povezavi preko USB: {error}",
|
||||||
|
"usbErrorConnectTimedOut": "Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion.",
|
||||||
|
"@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.",
|
||||||
|
"@radioStats_noiseFloor": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_lastRssi": {
|
||||||
|
"placeholders": {
|
||||||
|
"rssiDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_lastSnr": {
|
||||||
|
"placeholders": {
|
||||||
|
"snr": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_txAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_rxAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_stripNoise": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"appSettings_languageHu": "Madžarski",
|
||||||
|
"appSettings_jumpToOldestUnreadSubtitle": "Ko odpirate klepet z neprebranimi sporočili, se premaknite na prvo neprebrano sporočilo, namesto najnovejšega.",
|
||||||
|
"chat_sendCooldown": "Prosimo, počakajte trenutek, preden pošljete ponovno.",
|
||||||
|
"appSettings_jumpToOldestUnread": "Pritisnite za najstarejše nepročitano sporočilo",
|
||||||
|
"appSettings_languageJa": "Japonski",
|
||||||
|
"appSettings_languageKo": "Korejski",
|
||||||
|
"radioStats_tooltip": "Statistike za radio in mrežo",
|
||||||
|
"radioStats_notConnected": "Povežite se z napravo, da si ogledate statistiko o radiju.",
|
||||||
|
"radioStats_screenTitle": "Radijske statistike",
|
||||||
|
"radioStats_firmwareTooOld": "Statistika za radio zahteva združljivo programsko opremo v8 ali kasnejše.",
|
||||||
|
"radioStats_waiting": "Čakam na podatke…",
|
||||||
|
"radioStats_noiseFloor": "Število šuma: {noiseDbm} dBm",
|
||||||
|
"radioStats_lastRssi": "Najkasnejše vrednost RSSI: {rssiDbm} dBm",
|
||||||
|
"radioStats_lastSnr": "Najkasnejše vrednost SNR: {snr} dB",
|
||||||
|
"radioStats_txAir": "Čas na TX (skupno): {seconds} s",
|
||||||
|
"radioStats_rxAir": "Čas, namenjen RX-ju (skupno): {seconds} s",
|
||||||
|
"radioStats_chartCaption": "Ravnovredna raven šuma (dBm) za nedavne vzorce.",
|
||||||
|
"radioStats_stripNoise": "Število šuma: {noiseDbm} dBm",
|
||||||
|
"radioStats_stripWaiting": "Prejemanje statistike o radiju…",
|
||||||
|
"radioStats_settingsTile": "Radijske statistike",
|
||||||
|
"radioStats_settingsSubtitle": "Število šumov, RSSI, SNR in čas, ki ga je napolnila oprema",
|
||||||
|
"@translation_downloadFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_composerTitle": "Preprištejte, preden pošljete",
|
||||||
|
"translation_title": "Prevod",
|
||||||
|
"translation_enableSubtitle": "Prevedite vstopne sporočila in omogočite predhodno prevajanje.",
|
||||||
|
"translation_enableTitle": "Omogočite prevod",
|
||||||
|
"translation_composerSubtitle": "Ureja privzeto stanje ikone za prevod, ki jo uporablja avtor.",
|
||||||
|
"translation_targetLanguage": "Ciljna jezika",
|
||||||
|
"translation_useAppLanguage": "Uporabite jezik aplikacije",
|
||||||
|
"translation_downloadedModelLabel": "Naložen model",
|
||||||
|
"translation_presetModelLabel": "Prednastavljeni model Hugging Face",
|
||||||
|
"translation_manualUrlLabel": "URL za ročni model",
|
||||||
|
"translation_downloadModel": "Prenesite model",
|
||||||
|
"translation_downloading": "Izvajanje...",
|
||||||
|
"translation_working": "Delo...",
|
||||||
|
"translation_stop": "Prekliji",
|
||||||
|
"translation_mergingChunks": "Sklapljanje prenesenih delov v končni datoteko...",
|
||||||
|
"translation_downloadedModels": "Naloženi modeli",
|
||||||
|
"translation_deleteModel": "Izbrisati model",
|
||||||
|
"translation_modelDownloaded": "Model za prevajanje je bil naložen.",
|
||||||
|
"translation_downloadStopped": "Prenos je bil prekinjen.",
|
||||||
|
"translation_downloadFailed": "Izgovoritev ni bila uspešna: {error}",
|
||||||
|
"translation_enterUrlFirst": "Najprej vnesite URL model.",
|
||||||
|
"@scanner_linuxPairingPinPrompt": {
|
||||||
|
"placeholders": {
|
||||||
|
"deviceName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@translation_translateTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"language": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_translateBeforeSending": "Preprištejte, preden pošljete",
|
||||||
|
"translation_composerDisabledHint": "Pošljite sporočila v originalnem tipkanem jeziku.",
|
||||||
|
"translation_composerEnabledHint": "Vsebina sporočil bo prevedena, preden jih pošljemo.",
|
||||||
|
"translation_messageTranslation": "Prevod sporočila",
|
||||||
|
"translation_translateTo": "Prevesti v {language}",
|
||||||
|
"translation_translationOptions": "Možnosti prevoda",
|
||||||
|
"translation_systemLanguage": "Jezik sistema",
|
||||||
|
"scanner_linuxPairingShowPin": "Prikaži PIN",
|
||||||
|
"scanner_linuxPairingHidePin": "Skrij PIN",
|
||||||
|
"scanner_linuxPairingPinPrompt": "Vnesite PIN za {deviceName} (pustite prazno, če ga ni).",
|
||||||
|
"scanner_linuxPairingPinTitle": "Bluetooth PIN za seznanjanje",
|
||||||
|
"repeater_cliQuickDiscovery": "Odkrijte sosede",
|
||||||
|
"repeater_cliQuickClockSync": "Usklajevanje ure"
|
||||||
}
|
}
|
||||||
+469
-4
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"channels_channelDeleteFailed": "Det gick inte att ta bort kanalen \"{name}\"",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@@locale": "sv",
|
"@@locale": "sv",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Kontakter",
|
"nav_contacts": "Kontakter",
|
||||||
@@ -277,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": {
|
||||||
@@ -334,6 +343,8 @@
|
|||||||
"channels_publicChannel": "Allmänt kanal",
|
"channels_publicChannel": "Allmänt kanal",
|
||||||
"channels_privateChannel": "Privat kanal",
|
"channels_privateChannel": "Privat kanal",
|
||||||
"channels_editChannel": "Redigera kanal",
|
"channels_editChannel": "Redigera kanal",
|
||||||
|
"channels_muteChannel": "Tysta kanal",
|
||||||
|
"channels_unmuteChannel": "Slå på ljud för kanal",
|
||||||
"channels_deleteChannel": "Ta bort kanal",
|
"channels_deleteChannel": "Ta bort kanal",
|
||||||
"channels_deleteChannelConfirm": "Radera \"{name}\"? Detta kan inte ångras.",
|
"channels_deleteChannelConfirm": "Radera \"{name}\"? Detta kan inte ångras.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
@@ -1351,12 +1362,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Grannar",
|
"repeater_neighbors": "Grannar",
|
||||||
"repeater_neighboursSubtitle": "Visa noll hoppgrannar.",
|
"repeater_neighborsSubtitle": "Visa noll hoppgrannar.",
|
||||||
"neighbors_receivedData": "Mottagna grannars data",
|
"neighbors_receivedData": "Mottagna grannars data",
|
||||||
"neighbors_requestTimedOut": "Grannar begär tidsinställd utskick.",
|
"neighbors_requestTimedOut": "Grannar begär tidsinställd utskick.",
|
||||||
"neighbors_errorLoading": "Fel vid inläsning av grannar: {error}",
|
"neighbors_errorLoading": "Fel vid inläsning av grannar: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Upprepar grannar",
|
"neighbors_repeatersNeighbors": "Upprepar grannar",
|
||||||
"neighbors_noData": "Inga grannuppgifter finns tillgängliga.",
|
"neighbors_noData": "Inga grannuppgifter finns tillgängliga.",
|
||||||
"channels_createPrivateChannel": "Skapa en privat kanal",
|
"channels_createPrivateChannel": "Skapa en privat kanal",
|
||||||
"channels_joinPrivateChannel": "Gå med i en Privat Kanal",
|
"channels_joinPrivateChannel": "Gå med i en Privat Kanal",
|
||||||
@@ -1556,6 +1567,8 @@
|
|||||||
"contacts_copyAdvertToClipboard": "Kopiera annons till urklipp",
|
"contacts_copyAdvertToClipboard": "Kopiera annons till urklipp",
|
||||||
"contacts_invalidAdvertFormat": "Ogiltiga kontaktuppgifter",
|
"contacts_invalidAdvertFormat": "Ogiltiga kontaktuppgifter",
|
||||||
"appSettings_languageUk": "Ukrainska",
|
"appSettings_languageUk": "Ukrainska",
|
||||||
|
"appSettings_enableMessageTracing": "Aktivera meddelandespårning",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden",
|
||||||
"contacts_addContactFromClipboard": "Lägg till kontakt från urklipp",
|
"contacts_addContactFromClipboard": "Lägg till kontakt från urklipp",
|
||||||
"contacts_contactImported": "Kontakt har importerats.",
|
"contacts_contactImported": "Kontakt har importerats.",
|
||||||
"contacts_zeroHopContactAdvertSent": "Skickat kontakt via annons.",
|
"contacts_zeroHopContactAdvertSent": "Skickat kontakt via annons.",
|
||||||
@@ -1593,8 +1606,460 @@
|
|||||||
"map_removeLast": "Ta bort sista",
|
"map_removeLast": "Ta bort sista",
|
||||||
"scanner_enableBluetooth": "Aktivera Bluetooth",
|
"scanner_enableBluetooth": "Aktivera Bluetooth",
|
||||||
"scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.",
|
"scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.",
|
||||||
|
"scanner_chromeRequired": "Chrome-webbläsare krävs",
|
||||||
|
"scanner_chromeRequiredMessage": "Denna webbapplikation kräver Google Chrome oder en Chromium-baserader webbläsare för Bluetooth-stöd.",
|
||||||
"scanner_bluetoothOff": "Bluetooth är avstängt",
|
"scanner_bluetoothOff": "Bluetooth är avstängt",
|
||||||
|
"snrIndicator_lastSeen": "Senast sedd",
|
||||||
|
"snrIndicator_nearByRepeaters": "Närliggande uppreparstationer",
|
||||||
|
"chat_ShowAllPaths": "Visa alla vägar",
|
||||||
"settings_clientRepeatSubtitle": "Låt enheten repetera nätpaket för andra användare.",
|
"settings_clientRepeatSubtitle": "Låt enheten repetera nätpaket för andra användare.",
|
||||||
"settings_clientRepeat": "Upprepa utan elnät",
|
"settings_clientRepeat": "Upprepa utan elnät",
|
||||||
"settings_clientRepeatFreqWarning": "För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz."
|
"settings_clientRepeatFreqWarning": "För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz.",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "LOS-höjddata: Open-Meteo (CC BY 4.0)",
|
||||||
|
"appSettings_unitsTitle": "Enheter",
|
||||||
|
"appSettings_unitsMetric": "Metriskt (m/km)",
|
||||||
|
"appSettings_unitsImperial": "Imperialt (ft / mi)",
|
||||||
|
"map_lineOfSight": "Synlinje",
|
||||||
|
"map_losScreenTitle": "Synlinje",
|
||||||
|
"losSelectStartEnd": "Välj start- och slutnoder för LOS.",
|
||||||
|
"losRunFailed": "Synlinjekontroll misslyckades: {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Rensa alla punkter",
|
||||||
|
"losRunToViewElevationProfile": "Kör LOS för att se höjdprofil",
|
||||||
|
"losMenuTitle": "LOS-menyn",
|
||||||
|
"losMenuSubtitle": "Tryck på noder eller tryck länge på kartan för anpassade punkter",
|
||||||
|
"losShowDisplayNodes": "Visa displaynoder",
|
||||||
|
"losCustomPoints": "Anpassade poäng",
|
||||||
|
"losCustomPointLabel": "Anpassad {index}",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Punkt A",
|
||||||
|
"losPointB": "Punkt B",
|
||||||
|
"losAntennaA": "Antenn A: {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Antenn B: {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Kör LOS",
|
||||||
|
"losNoElevationData": "Inga höjddata",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, rensa LOS, min clearance {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, blockerad av {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "LOS: kollar...",
|
||||||
|
"losStatusNoData": "LOS: inga data",
|
||||||
|
"losStatusSummary": "LOS: {clear}/{total} rensa, {blocked} blockerad, {unknown} okänd",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Höjddata är inte tillgänglig för ett eller flera prover.",
|
||||||
|
"losErrorInvalidInput": "Ogiltiga poäng/höjddata för LOS-beräkning.",
|
||||||
|
"losRenameCustomPoint": "Byt namn på anpassad punkt",
|
||||||
|
"losPointName": "Punktnamn",
|
||||||
|
"losShowPanelTooltip": "Visa LOS-panelen",
|
||||||
|
"losHidePanelTooltip": "Dölj LOS-panelen",
|
||||||
|
"losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Radiohorisont",
|
||||||
|
"losLegendLosBeam": "Siktlinje",
|
||||||
|
"losLegendTerrain": "Terräng",
|
||||||
|
"losFrequencyLabel": "Frekvens",
|
||||||
|
"losFrequencyInfoTooltip": "Visa detaljer om beräkningen",
|
||||||
|
"losFrequencyDialogTitle": "Beräkning av radiohorisonten",
|
||||||
|
"losFrequencyDialogDescription": "Med start från k={baselineK} vid {baselineFreq} MHz, justerar beräkningen k-faktorn för det aktuella {frequencyMHz} MHz-bandet, som definierar den böjda radiohorisonten.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listFilter_removeFromFavorites": "Ta bort från favoriter",
|
||||||
|
"listFilter_addToFavorites": "Lägg till i favoriter",
|
||||||
|
"listFilter_favorites": "Favoriter",
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_unread": "Oläst",
|
||||||
|
"contacts_searchContactsNoNumber": "Sök kontakter...",
|
||||||
|
"contacts_searchRepeaters": "Sök {number}{str} upprepningsenheter...",
|
||||||
|
"contacts_searchFavorites": "Sök {number}{str} Favoriter...",
|
||||||
|
"contacts_searchUsers": "Sök {number}{str} användare...",
|
||||||
|
"contacts_searchRoomServers": "Sök {number}{str} Room-servrar...",
|
||||||
|
"settings_contactSettingsSubtitle": "Inställningar för hur kontakter läggs till.",
|
||||||
|
"settings_contactSettings": "Kontaktinställningar",
|
||||||
|
"contactsSettings_autoAddTitle": "Automatisk upptäckt",
|
||||||
|
"contactsSettings_otherTitle": "Andra inställningar relaterade till kontakt",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta användare",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Lägg till upprepande enheter automatiskt",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta rumsservrar.",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Lägg till sensorer automatiskt",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Lägg till användare automatiskt",
|
||||||
|
"contactsSettings_title": "Kontaktinställningar",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta sensorer.",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Skriv över äldst",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta repeater.",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Lägg automatiskt till rumsservrar",
|
||||||
|
"discoveredContacts_noMatching": "Inga matchande kontakter",
|
||||||
|
"discoveredContacts_searchHint": "Sök uppfunna kontakter",
|
||||||
|
"discoveredContacts_deleteContact": "Ta bort kontakt",
|
||||||
|
"discoveredContacts_Title": "Upptäckta kontakter",
|
||||||
|
"discoveredContacts_contactAdded": "Kontakt tillagd",
|
||||||
|
"discoveredContacts_addContact": "Lägg till kontakt",
|
||||||
|
"discoveredContacts_copyContact": "Kopiera kontakt till urklipp",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "När kontaktlistan är full ersätts den äldsta icke-favoriterade kontakten.",
|
||||||
|
"common_deleteAll": "Ta bort alla",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Är du säker på att du vill ta bort alla upptäckta kontakter?",
|
||||||
|
"discoveredContacts_deleteContactAll": "Ta bort alla upptäckta kontakter",
|
||||||
|
"map_guessedLocation": "Gissad plats",
|
||||||
|
"map_showGuessedLocations": "Visa upp de antagna nodernas placeringar",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenSubtitle": "Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.",
|
||||||
|
"usbScreenTitle": "Anslut via USB",
|
||||||
|
"usbScreenStatus": "Välj en USB-enhet",
|
||||||
|
"usbScreenNote": "USB-seriell kommunikation är aktiv på stödda Android-enheter och på skrivbordsplattformar.",
|
||||||
|
"usbScreenEmptyState": "Inga USB-enheter hittades. Anslut en och uppdatera.",
|
||||||
|
"usbErrorPermissionDenied": "Tillgången via USB nekas.",
|
||||||
|
"usbErrorDeviceMissing": "Den valda USB-enheten är inte längre tillgänglig.",
|
||||||
|
"usbErrorInvalidPort": "Välj en giltig USB-enhet.",
|
||||||
|
"usbErrorBusy": "En annan förfrågan om USB-anslutning är redan pågående.",
|
||||||
|
"usbErrorNotConnected": "Ingen USB-enhet är ansluten.",
|
||||||
|
"usbErrorOpenFailed": "Misslyckades med att öppna det valda USB-enheten.",
|
||||||
|
"usbErrorConnectFailed": "Kunde inte ansluta till det valda USB-enheten.",
|
||||||
|
"usbErrorUnsupported": "USB-seriell kommunikation stöds inte på denna plattform.",
|
||||||
|
"usbErrorAlreadyActive": "En USB-anslutning är redan aktiv.",
|
||||||
|
"usbErrorNoDeviceSelected": "Ingen USB-enhet valdes.",
|
||||||
|
"usbErrorPortClosed": "USB-anslutningen är inte aktiv.",
|
||||||
|
"usbFallbackDeviceName": "Web-serieenhet",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_connecting": "Anslutning till USB-enhet...",
|
||||||
|
"usbStatus_notConnected": "Välj en USB-enhet",
|
||||||
|
"usbConnectionFailed": "Fel vid USB-anslutning: {error}",
|
||||||
|
"usbStatus_searching": "Söker efter USB-enheter...",
|
||||||
|
"usbErrorConnectTimedOut": "Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware.",
|
||||||
|
"@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",
|
||||||
|
"@radioStats_noiseFloor": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_lastRssi": {
|
||||||
|
"placeholders": {
|
||||||
|
"rssiDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_lastSnr": {
|
||||||
|
"placeholders": {
|
||||||
|
"snr": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_txAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_rxAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_stripNoise": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"appSettings_jumpToOldestUnreadSubtitle": "När du öppnar en chatt med oinlästa meddelanden, scrolla till det första oinlästa meddelandet istället för det senaste.",
|
||||||
|
"chat_sendCooldown": "Vänligen vänta en stund innan du skickar igen.",
|
||||||
|
"appSettings_jumpToOldestUnread": "Gå direkt till det äldsta, obesvarade meddelandet",
|
||||||
|
"appSettings_languageHu": "Ungerskt",
|
||||||
|
"appSettings_languageJa": "Japanska",
|
||||||
|
"appSettings_languageKo": "Koreanska",
|
||||||
|
"radioStats_tooltip": "Radio- och mesh-statistik",
|
||||||
|
"radioStats_screenTitle": "Radiostation",
|
||||||
|
"radioStats_notConnected": "Anslut till en enhet för att visa radiostatistik.",
|
||||||
|
"radioStats_firmwareTooOld": "Radio statistik kräver kompatibel firmware version 8 eller senare.",
|
||||||
|
"radioStats_waiting": "Väntar på data…",
|
||||||
|
"radioStats_noiseFloor": "Bakgrundsnivå: {noiseDbm} dBm",
|
||||||
|
"radioStats_lastRssi": "Senaste RSSI-värde: {rssiDbm} dBm",
|
||||||
|
"radioStats_lastSnr": "Senaste SNR: {snr} dB",
|
||||||
|
"radioStats_txAir": "TX-tid (total): {seconds} sekunder",
|
||||||
|
"radioStats_rxAir": "RX-tid (total): {seconds} s",
|
||||||
|
"radioStats_chartCaption": "Ljudnivå (dBm) baserat på de senaste mätningarna.",
|
||||||
|
"radioStats_stripNoise": "Bakgrundsnivå: {noiseDbm} dBm",
|
||||||
|
"radioStats_stripWaiting": "Hämtar radiostatistik…",
|
||||||
|
"radioStats_settingsTile": "Radiostation",
|
||||||
|
"radioStats_settingsSubtitle": "Bakgrundsnivå, RSSI, SNR och tillgänglig tid",
|
||||||
|
"@translation_downloadFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_enableSubtitle": "Översätt inkommande meddelanden och möjliggör översättning före avsändning.",
|
||||||
|
"translation_enableTitle": "Aktivera översättning",
|
||||||
|
"translation_title": "Översättning",
|
||||||
|
"translation_composerTitle": "Översätt innan du skickar",
|
||||||
|
"translation_composerSubtitle": "Styr standardtillståndet för kompositorns översättningsikon.",
|
||||||
|
"translation_targetLanguage": "Målmedvetet språk",
|
||||||
|
"translation_useAppLanguage": "Använd appens språk",
|
||||||
|
"translation_downloadedModelLabel": "Nedladdad modell",
|
||||||
|
"translation_presetModelLabel": "Fördefinierat Hugging Face-modell",
|
||||||
|
"translation_manualUrlLabel": "Manualens URL",
|
||||||
|
"translation_downloadModel": "Ladda ner modellen",
|
||||||
|
"translation_downloading": "Nedladdning...",
|
||||||
|
"translation_working": "Arbeta...",
|
||||||
|
"translation_stop": "Stopp",
|
||||||
|
"translation_mergingChunks": "Slå samman de nedladdade delarna till en slutlig fil...",
|
||||||
|
"translation_downloadedModels": "Nedladdade modeller",
|
||||||
|
"translation_deleteModel": "Ta bort modell",
|
||||||
|
"translation_modelDownloaded": "Översättningsmodellen har laddats ner.",
|
||||||
|
"translation_downloadStopped": "Nedladdningen avbruten.",
|
||||||
|
"translation_downloadFailed": "Nedladdning misslyckades: {error}",
|
||||||
|
"translation_enterUrlFirst": "Ange först en URL för en specifik modell.",
|
||||||
|
"@scanner_linuxPairingPinPrompt": {
|
||||||
|
"placeholders": {
|
||||||
|
"deviceName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@translation_translateTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"language": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_composerDisabledHint": "Skicka meddelanden på det ursprungliga, stavade språket.",
|
||||||
|
"translation_translateBeforeSending": "Översätt innan du skickar",
|
||||||
|
"translation_composerEnabledHint": "Meddelandena kommer att översättas innan de skickas.",
|
||||||
|
"translation_messageTranslation": "Meddelandets översättning",
|
||||||
|
"translation_translateTo": "Översätt till {language}",
|
||||||
|
"translation_translationOptions": "Översättningsalternativ",
|
||||||
|
"translation_systemLanguage": "Språk för systemet",
|
||||||
|
"scanner_linuxPairingShowPin": "Visa PIN",
|
||||||
|
"scanner_linuxPairingPinTitle": "Bluetooth‑parnings‑PIN",
|
||||||
|
"scanner_linuxPairingPinPrompt": "Ange PIN för {deviceName} (lämna tomt om ingen).",
|
||||||
|
"scanner_linuxPairingHidePin": "Dölj PIN",
|
||||||
|
"repeater_cliQuickDiscovery": "Upptäck grannar",
|
||||||
|
"repeater_cliQuickClockSync": "Synkronisera klocka"
|
||||||
}
|
}
|
||||||
+469
-4
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"channels_channelDeleteFailed": "Не вдалося видалити канал \"{name}\"",
|
||||||
|
"@channels_channelDeleteFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@@locale": "uk",
|
"@@locale": "uk",
|
||||||
"appTitle": "MeshCore Open",
|
"appTitle": "MeshCore Open",
|
||||||
"nav_contacts": "Контакти",
|
"nav_contacts": "Контакти",
|
||||||
@@ -278,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": {
|
||||||
@@ -335,6 +344,8 @@
|
|||||||
"channels_publicChannel": "Публічний канал",
|
"channels_publicChannel": "Публічний канал",
|
||||||
"channels_privateChannel": "Приватний канал",
|
"channels_privateChannel": "Приватний канал",
|
||||||
"channels_editChannel": "Редагувати канал",
|
"channels_editChannel": "Редагувати канал",
|
||||||
|
"channels_muteChannel": "Вимкнути сповіщення каналу",
|
||||||
|
"channels_unmuteChannel": "Увімкнути сповіщення каналу",
|
||||||
"channels_deleteChannel": "Видалити канал",
|
"channels_deleteChannel": "Видалити канал",
|
||||||
"channels_deleteChannelConfirm": "Видалити {name}? Це не можна скасувати.",
|
"channels_deleteChannelConfirm": "Видалити {name}? Це не можна скасувати.",
|
||||||
"@channels_deleteChannelConfirm": {
|
"@channels_deleteChannelConfirm": {
|
||||||
@@ -1352,12 +1363,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repeater_neighbours": "Сусіди",
|
"repeater_neighbors": "Сусіди",
|
||||||
"repeater_neighboursSubtitle": "Показати сусідів нульового стрибка.",
|
"repeater_neighborsSubtitle": "Показати сусідів нульового стрибка.",
|
||||||
"neighbors_receivedData": "Дані сусідів отримано",
|
"neighbors_receivedData": "Дані сусідів отримано",
|
||||||
"neighbors_requestTimedOut": "Час запиту сусідів вичерпано.",
|
"neighbors_requestTimedOut": "Час запиту сусідів вичерпано.",
|
||||||
"neighbors_errorLoading": "Помилка завантаження сусідів: {error}",
|
"neighbors_errorLoading": "Помилка завантаження сусідів: {error}",
|
||||||
"neighbors_repeatersNeighbours": "Ретранслятори-сусіди",
|
"neighbors_repeatersNeighbors": "Ретранслятори-сусіди",
|
||||||
"neighbors_noData": "Дані про сусідів недоступні.",
|
"neighbors_noData": "Дані про сусідів недоступні.",
|
||||||
"channels_createPrivateChannelDesc": "Захищено секретним ключем.",
|
"channels_createPrivateChannelDesc": "Захищено секретним ключем.",
|
||||||
"channels_joinPrivateChannel": "Приєднатися до приватного каналу",
|
"channels_joinPrivateChannel": "Приєднатися до приватного каналу",
|
||||||
@@ -1557,6 +1568,8 @@
|
|||||||
"contacts_copyAdvertToClipboard": "Копіювати оголошення в буфер обміну",
|
"contacts_copyAdvertToClipboard": "Копіювати оголошення в буфер обміну",
|
||||||
"contacts_clipboardEmpty": "Буфер обміну порожній",
|
"contacts_clipboardEmpty": "Буфер обміну порожній",
|
||||||
"appSettings_languageRu": "Російська",
|
"appSettings_languageRu": "Російська",
|
||||||
|
"appSettings_enableMessageTracing": "Увімкнути відстеження повідомлень",
|
||||||
|
"appSettings_enableMessageTracingSubtitle": "Показувати детальні метадані про маршрутизацію та час для повідомлень",
|
||||||
"contacts_ShareContact": "Копіювати контакт у буфер обміну",
|
"contacts_ShareContact": "Копіювати контакт у буфер обміну",
|
||||||
"contacts_zeroHopContactAdvertFailed": "Не вдалося надіслати контакт.",
|
"contacts_zeroHopContactAdvertFailed": "Не вдалося надіслати контакт.",
|
||||||
"contacts_contactAdvertCopied": "Рекламу скопійовано до буфера обміну.",
|
"contacts_contactAdvertCopied": "Рекламу скопійовано до буфера обміну.",
|
||||||
@@ -1593,8 +1606,460 @@
|
|||||||
"map_pathTraceCancelled": "Відмінується трасування шляху",
|
"map_pathTraceCancelled": "Відмінується трасування шляху",
|
||||||
"scanner_enableBluetooth": "Увімкніть Bluetooth",
|
"scanner_enableBluetooth": "Увімкніть Bluetooth",
|
||||||
"scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.",
|
"scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.",
|
||||||
|
"scanner_chromeRequired": "Потрібен браузер Chrome",
|
||||||
|
"scanner_chromeRequiredMessage": "Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.",
|
||||||
"scanner_bluetoothOff": "Bluetooth вимкнено",
|
"scanner_bluetoothOff": "Bluetooth вимкнено",
|
||||||
|
"snrIndicator_lastSeen": "Останній раз бачили",
|
||||||
|
"snrIndicator_nearByRepeaters": "Ближні ретранслятори",
|
||||||
|
"chat_ShowAllPaths": "Показати всі шляхи",
|
||||||
"settings_clientRepeatFreqWarning": "Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.",
|
"settings_clientRepeatFreqWarning": "Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.",
|
||||||
"settings_clientRepeatSubtitle": "Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.",
|
"settings_clientRepeatSubtitle": "Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.",
|
||||||
"settings_clientRepeat": "Автономна система"
|
"settings_clientRepeat": "Автономна система",
|
||||||
|
"settings_aboutOpenMeteoAttribution": "Дані про висоту LOS: Open-Meteo (CC BY 4.0)",
|
||||||
|
"appSettings_unitsTitle": "одиниці",
|
||||||
|
"appSettings_unitsMetric": "Метричний (м / км)",
|
||||||
|
"appSettings_unitsImperial": "Імперська (ft / mi)",
|
||||||
|
"map_lineOfSight": "Пряма видимість",
|
||||||
|
"map_losScreenTitle": "Пряма видимість",
|
||||||
|
"losSelectStartEnd": "Виберіть початковий і кінцевий вузли для LOS.",
|
||||||
|
"losRunFailed": "Помилка перевірки прямої видимості: {error}",
|
||||||
|
"@losRunFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losClearAllPoints": "Очистити всі пункти",
|
||||||
|
"losRunToViewElevationProfile": "Запустіть LOS, щоб переглянути профіль висоти",
|
||||||
|
"losMenuTitle": "Меню LOS",
|
||||||
|
"losMenuSubtitle": "Торкніться вузлів або утримуйте карту, щоб отримати власні точки",
|
||||||
|
"losShowDisplayNodes": "Показати вузли відображення",
|
||||||
|
"losCustomPoints": "Користувальницькі точки",
|
||||||
|
"losCustomPointLabel": "Спеціальний {index}",
|
||||||
|
"@losCustomPointLabel": {
|
||||||
|
"placeholders": {
|
||||||
|
"index": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losPointA": "Точка А",
|
||||||
|
"losPointB": "Точка Б",
|
||||||
|
"losAntennaA": "Антена A: {value} {unit}",
|
||||||
|
"@losAntennaA": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losAntennaB": "Антена B: {value} {unit}",
|
||||||
|
"@losAntennaB": {
|
||||||
|
"placeholders": {
|
||||||
|
"value": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losRun": "Запустіть LOS",
|
||||||
|
"losNoElevationData": "Немає даних про висоту",
|
||||||
|
"losProfileClear": "{distance} {distanceUnit}, чистий LOS, мінімальний зазор {clearance} {heightUnit}",
|
||||||
|
"@losProfileClear": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losProfileBlocked": "{distance} {distanceUnit}, заблоковано {obstruction} {heightUnit}",
|
||||||
|
"@losProfileBlocked": {
|
||||||
|
"placeholders": {
|
||||||
|
"distance": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"distanceUnit": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"obstruction": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"heightUnit": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losStatusChecking": "LOS: перевірка...",
|
||||||
|
"losStatusNoData": "LOS: немає даних",
|
||||||
|
"losStatusSummary": "LOS: {clear}/{total} очищено, {blocked} заблоковано, {unknown} невідомо",
|
||||||
|
"@losStatusSummary": {
|
||||||
|
"placeholders": {
|
||||||
|
"clear": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"unknown": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"losErrorElevationUnavailable": "Дані про висоту недоступні для одного чи кількох зразків.",
|
||||||
|
"losErrorInvalidInput": "Недійсні дані про точки/висоту для розрахунку LOS.",
|
||||||
|
"losRenameCustomPoint": "Перейменуйте спеціальну точку",
|
||||||
|
"losPointName": "Назва точки",
|
||||||
|
"losShowPanelTooltip": "Показати панель LOS",
|
||||||
|
"losHidePanelTooltip": "Приховати панель LOS",
|
||||||
|
"losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)",
|
||||||
|
"losLegendRadioHorizon": "Радіогоризонт",
|
||||||
|
"losLegendLosBeam": "Лінія прямої видимості",
|
||||||
|
"losLegendTerrain": "Рельєф",
|
||||||
|
"losFrequencyLabel": "Частота",
|
||||||
|
"losFrequencyInfoTooltip": "Переглянути деталі розрахунку",
|
||||||
|
"losFrequencyDialogTitle": "Розрахунок радіогоризонту",
|
||||||
|
"losFrequencyDialogDescription": "Починаючи з k={baselineK} на {baselineFreq} МГц, обчислення коригує k-фактор для поточного діапазону {frequencyMHz} МГц, який визначає викривлену межу радіогоризонту.",
|
||||||
|
"@losFrequencyDialogDescription": {
|
||||||
|
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||||
|
"placeholders": {
|
||||||
|
"baselineK": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"baselineFreq": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"frequencyMHz": {
|
||||||
|
"type": "double"
|
||||||
|
},
|
||||||
|
"kFactor": {
|
||||||
|
"type": "double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listFilter_removeFromFavorites": "Видалити зі списку улюблених",
|
||||||
|
"listFilter_addToFavorites": "Додати до улюблених",
|
||||||
|
"listFilter_favorites": "Улюблені",
|
||||||
|
"@contacts_searchFavorites": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchUsers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRepeaters": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@contacts_searchRoomServers": {
|
||||||
|
"placeholders": {
|
||||||
|
"number": {
|
||||||
|
"type": "int"
|
||||||
|
},
|
||||||
|
"str": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contacts_searchRoomServers": "Пошук {number}{str} серверів кімнат...",
|
||||||
|
"contacts_searchUsers": "Пошук {number}{str} користувачів...",
|
||||||
|
"contacts_searchFavorites": "Пошук {number}{str} улюблених...",
|
||||||
|
"contacts_searchContactsNoNumber": "Пошук контактів...",
|
||||||
|
"contacts_searchRepeaters": "Пошук {number}{str} ретрансляторів...",
|
||||||
|
"contacts_unread": "Непрочитане",
|
||||||
|
"settings_contactSettingsSubtitle": "Налаштування для додавання контактів",
|
||||||
|
"settings_contactSettings": "Налаштування контактів",
|
||||||
|
"contactsSettings_autoAddUsersSubtitle": "Дозволити супутникові автоматично додавати виявлених користувачів",
|
||||||
|
"contactsSettings_autoAddRepeatersTitle": "Автоматично додавати повторювачі",
|
||||||
|
"contactsSettings_autoAddRepeatersSubtitle": "Дозволити супутнику автоматично додавати виявлені ретранслятори",
|
||||||
|
"contactsSettings_autoAddRoomServersTitle": "Автоматично додавати сервери кімнат",
|
||||||
|
"contactsSettings_otherTitle": "Інші налаштування, пов'язані з контактами",
|
||||||
|
"contactsSettings_autoAddTitle": "Автоматичне виявлення",
|
||||||
|
"contactsSettings_autoAddUsersTitle": "Автоматично додавати користувачів",
|
||||||
|
"contactsSettings_title": "Налаштування контактів",
|
||||||
|
"contactsSettings_autoAddRoomServersSubtitle": "Дозволити супровіднику автоматично додавати виявлені сервери кімнат.",
|
||||||
|
"contactsSettings_autoAddSensorsTitle": "Автоматично додавати датчики",
|
||||||
|
"discoveredContacts_searchHint": "Знайти виявлені контакти",
|
||||||
|
"discoveredContacts_contactAdded": "Контакт додано",
|
||||||
|
"contactsSettings_autoAddSensorsSubtitle": "Дозволити супровіднику автоматично додавати виявлені сенсори",
|
||||||
|
"contactsSettings_overwriteOldestTitle": "Перезаписати найстаріше",
|
||||||
|
"discoveredContacts_Title": "Виявлені контакти",
|
||||||
|
"discoveredContacts_noMatching": "Відповідних контактів не знайдено",
|
||||||
|
"discoveredContacts_deleteContact": "Видалити контакт",
|
||||||
|
"discoveredContacts_copyContact": "Копіювати контакт у буфер обміну",
|
||||||
|
"discoveredContacts_addContact": "Додати контакт",
|
||||||
|
"contactsSettings_overwriteOldestSubtitle": "Коли список контактів заповнений, найстарший контакт без позначки улюбленого буде замінений.",
|
||||||
|
"common_deleteAll": "Видалити все",
|
||||||
|
"discoveredContacts_deleteContactAll": "Видалити всі виявлені контакти",
|
||||||
|
"discoveredContacts_deleteContactAllContent": "Ви впевнені, що хочете видалити всі виявлені контакти?",
|
||||||
|
"map_showGuessedLocations": "Показати місцезнаходження передбачених вузлів",
|
||||||
|
"map_guessedLocation": "Визначено місцезнаходження",
|
||||||
|
"usbScreenSubtitle": "Виберіть виявлене серійне пристрій і підключіть його безпосередньо до вашого вузла MeshCore.",
|
||||||
|
"usbScreenTitle": "Підключити через USB",
|
||||||
|
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||||
|
"connectionChoiceUsbLabel": "USB",
|
||||||
|
"usbScreenStatus": "Виберіть пристрій USB",
|
||||||
|
"usbScreenNote": "USB-серіальний інтерфейс активний на підтримуваних пристроях на базі Android та на десктопних платформах.",
|
||||||
|
"usbScreenEmptyState": "Не знайдено жодних пристроїв USB. Підключіть один і перезавантажте.",
|
||||||
|
"usbErrorPermissionDenied": "Було відмовлено у наданні дозволу на використання USB.",
|
||||||
|
"usbErrorDeviceMissing": "Вибране USB-пристрій більше недоступне.",
|
||||||
|
"usbErrorInvalidPort": "Виберіть дійсний USB-пристрій.",
|
||||||
|
"usbErrorBusy": "Ще один запит на підключення через USB вже обробляється.",
|
||||||
|
"usbErrorNotConnected": "Немає підключених пристроїв USB.",
|
||||||
|
"usbErrorOpenFailed": "Не вдалося відкрити вибране USB-пристрій.",
|
||||||
|
"usbErrorConnectFailed": "Не вдалося підключитися до вибраного USB-пристрою.",
|
||||||
|
"usbErrorUnsupported": "Підтримка USB-серіального інтерфейсу не реалізована на цій платформі.",
|
||||||
|
"usbErrorAlreadyActive": "USB-з'єднання вже встановлено.",
|
||||||
|
"usbErrorNoDeviceSelected": "Не було вибрано жодного пристрою USB.",
|
||||||
|
"usbErrorPortClosed": "З'єднання USB не встановлено.",
|
||||||
|
"usbFallbackDeviceName": "Пристрій для передачі даних по веб-серіалах",
|
||||||
|
"@usbConnectionFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usbStatus_searching": "Пошук пристроїв USB...",
|
||||||
|
"usbStatus_notConnected": "Виберіть пристрій USB",
|
||||||
|
"usbConnectionFailed": "Не вдалося встановити з'єднання через USB: {error}",
|
||||||
|
"usbStatus_connecting": "Підключення до USB-пристрою...",
|
||||||
|
"usbErrorConnectTimedOut": "З'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion.",
|
||||||
|
"@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": "Повернутися назад тим же шляхом",
|
||||||
|
"@radioStats_noiseFloor": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_lastRssi": {
|
||||||
|
"placeholders": {
|
||||||
|
"rssiDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_lastSnr": {
|
||||||
|
"placeholders": {
|
||||||
|
"snr": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_txAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_rxAir": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@radioStats_stripNoise": {
|
||||||
|
"placeholders": {
|
||||||
|
"noiseDbm": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chat_sendCooldown": "Будь ласка, зачекайте трохи, перш ніж відправляти знову.",
|
||||||
|
"appSettings_languageHu": "Угорський",
|
||||||
|
"appSettings_jumpToOldestUnreadSubtitle": "При відкритті чату з не прочитаними повідомленнями, прокрутіть до першого не прочитаного повідомлення, а не до останнього.",
|
||||||
|
"appSettings_jumpToOldestUnread": "Перейти до найстарішого непрочитаного повідомлення",
|
||||||
|
"appSettings_languageJa": "Японська",
|
||||||
|
"appSettings_languageKo": "Кореєська",
|
||||||
|
"radioStats_tooltip": "Статистика радіо та мережі",
|
||||||
|
"radioStats_screenTitle": "Дані про радіостанції",
|
||||||
|
"radioStats_notConnected": "Підключіться до пристрою, щоб переглядати статистику радіопередач.",
|
||||||
|
"radioStats_firmwareTooOld": "Статистика радіо приймача вимагає супутнього програмного забезпечення версії 8 або новішої.",
|
||||||
|
"radioStats_waiting": "Очікую на отримання даних…",
|
||||||
|
"radioStats_noiseFloor": "Рівень шуму: {noiseDbm} дБм",
|
||||||
|
"radioStats_lastRssi": "Останній показник RSSI: {rssiDbm} дБм",
|
||||||
|
"radioStats_lastSnr": "Останній показник SNR: {snr} дБ",
|
||||||
|
"radioStats_txAir": "Час трансляції на телеканалі TX (загальний): {seconds} секунд",
|
||||||
|
"radioStats_rxAir": "Загальний час використання RX: {seconds} секунд",
|
||||||
|
"radioStats_chartCaption": "Рівень шуму (дБм) на основі останніх вимірювань.",
|
||||||
|
"radioStats_stripNoise": "Рівень шуму: {noiseDbm} дБм",
|
||||||
|
"radioStats_stripWaiting": "Отримано статистику радіо…",
|
||||||
|
"radioStats_settingsTile": "Дані про радіостанції",
|
||||||
|
"radioStats_settingsSubtitle": "Рівень шуму, RSSI, SNR та час, протягом якого пристрій використовує радіоканал.",
|
||||||
|
"@translation_downloadFailed": {
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_composerTitle": "Перекладіть перед відправкою",
|
||||||
|
"translation_title": "Переклад",
|
||||||
|
"translation_enableTitle": "Увімкнути переклад",
|
||||||
|
"translation_enableSubtitle": "Перекладати отримані повідомлення та дозволяти попередній переклад перед відправкою.",
|
||||||
|
"translation_composerSubtitle": "Контролює стан ікон перекладу, який використовується за замовчуванням.",
|
||||||
|
"translation_targetLanguage": "Цільова мова",
|
||||||
|
"translation_useAppLanguage": "Використовуйте мову додатку",
|
||||||
|
"translation_downloadedModelLabel": "Завантажений шаблон",
|
||||||
|
"translation_presetModelLabel": "Заздалегідь налаштований модель від Hugging Face",
|
||||||
|
"translation_manualUrlLabel": "Посилання на веб-сторінку з інструкцією",
|
||||||
|
"translation_downloadModel": "Завантажити модель",
|
||||||
|
"translation_downloading": "Завантаження...",
|
||||||
|
"translation_working": "Працюю...",
|
||||||
|
"translation_stop": "Припинити",
|
||||||
|
"translation_mergingChunks": "Об'єднання завантажених фрагментів у кінцевий файл...",
|
||||||
|
"translation_downloadedModels": "Завантажені моделі",
|
||||||
|
"translation_deleteModel": "Видалити модель",
|
||||||
|
"translation_modelDownloaded": "Модель перекладу завантажена.",
|
||||||
|
"translation_downloadStopped": "Завантаження призупинено.",
|
||||||
|
"translation_downloadFailed": "Не вдалося завантажити: {error}",
|
||||||
|
"translation_enterUrlFirst": "Спочатку введіть URL моделі.",
|
||||||
|
"@scanner_linuxPairingPinPrompt": {
|
||||||
|
"placeholders": {
|
||||||
|
"deviceName": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@translation_translateTo": {
|
||||||
|
"placeholders": {
|
||||||
|
"language": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"translation_composerEnabledHint": "Повідомлення будуть перекладені перед відправленням.",
|
||||||
|
"translation_messageTranslation": "Переклад повідомлення",
|
||||||
|
"translation_composerDisabledHint": "Надсилайте повідомлення, використовуючи оригінальний текстовий формат.",
|
||||||
|
"translation_translateBeforeSending": "Перекладіть перед відправкою",
|
||||||
|
"translation_translateTo": "Перекласти на {language}",
|
||||||
|
"translation_translationOptions": "Варіанти перекладу",
|
||||||
|
"translation_systemLanguage": "Мова системи",
|
||||||
|
"scanner_linuxPairingPinTitle": "PIN‑код спарювання Bluetooth",
|
||||||
|
"scanner_linuxPairingShowPin": "Показати PIN",
|
||||||
|
"scanner_linuxPairingPinPrompt": "Введіть PIN для {deviceName} (залиште порожнім, якщо його немає).",
|
||||||
|
"scanner_linuxPairingHidePin": "Приховати PIN",
|
||||||
|
"repeater_cliQuickClockSync": "Синхронізація годинника",
|
||||||
|
"repeater_cliQuickDiscovery": "Відкрити сусідів"
|
||||||
}
|
}
|
||||||
+955
-485
File diff suppressed because it is too large
Load Diff
+60
-1
@@ -1,8 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'l10n/app_localizations.dart';
|
import 'l10n/app_localizations.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'screens/chrome_required_screen.dart';
|
||||||
|
import 'utils/platform_info.dart';
|
||||||
|
|
||||||
import 'connector/meshcore_connector.dart';
|
import 'connector/meshcore_connector.dart';
|
||||||
import 'screens/scanner_screen.dart';
|
import 'screens/scanner_screen.dart';
|
||||||
import 'services/storage_service.dart';
|
import 'services/storage_service.dart';
|
||||||
@@ -14,6 +18,10 @@ import 'services/ble_debug_log_service.dart';
|
|||||||
import 'services/app_debug_log_service.dart';
|
import 'services/app_debug_log_service.dart';
|
||||||
import 'services/background_service.dart';
|
import 'services/background_service.dart';
|
||||||
import 'services/map_tile_cache_service.dart';
|
import 'services/map_tile_cache_service.dart';
|
||||||
|
import 'services/chat_text_scale_service.dart';
|
||||||
|
import 'services/translation_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';
|
||||||
|
|
||||||
@@ -33,6 +41,10 @@ void main() async {
|
|||||||
final appDebugLogService = AppDebugLogService();
|
final appDebugLogService = AppDebugLogService();
|
||||||
final backgroundService = BackgroundService();
|
final backgroundService = BackgroundService();
|
||||||
final mapTileCacheService = MapTileCacheService();
|
final mapTileCacheService = MapTileCacheService();
|
||||||
|
final chatTextScaleService = ChatTextScaleService();
|
||||||
|
final translationService = TranslationService(appSettingsService);
|
||||||
|
final uiViewStateService = UiViewStateService();
|
||||||
|
final timeoutPredictionService = TimeoutPredictionService(storage);
|
||||||
|
|
||||||
// Load settings
|
// Load settings
|
||||||
await appSettingsService.loadSettings();
|
await appSettingsService.loadSettings();
|
||||||
@@ -47,15 +59,23 @@ void main() async {
|
|||||||
final notificationService = NotificationService();
|
final notificationService = NotificationService();
|
||||||
await notificationService.initialize();
|
await notificationService.initialize();
|
||||||
await backgroundService.initialize();
|
await backgroundService.initialize();
|
||||||
|
_registerThirdPartyLicenses();
|
||||||
|
|
||||||
|
await chatTextScaleService.initialize();
|
||||||
|
await translationService.refreshDownloadedModels();
|
||||||
|
await uiViewStateService.initialize();
|
||||||
|
await timeoutPredictionService.initialize();
|
||||||
|
|
||||||
// Wire up connector with services
|
// Wire up connector with services
|
||||||
connector.initialize(
|
connector.initialize(
|
||||||
retryService: retryService,
|
retryService: retryService,
|
||||||
pathHistoryService: pathHistoryService,
|
pathHistoryService: pathHistoryService,
|
||||||
appSettingsService: appSettingsService,
|
appSettingsService: appSettingsService,
|
||||||
|
translationService: translationService,
|
||||||
bleDebugLogService: bleDebugLogService,
|
bleDebugLogService: bleDebugLogService,
|
||||||
appDebugLogService: appDebugLogService,
|
appDebugLogService: appDebugLogService,
|
||||||
backgroundService: backgroundService,
|
backgroundService: backgroundService,
|
||||||
|
timeoutPredictionService: timeoutPredictionService,
|
||||||
);
|
);
|
||||||
|
|
||||||
await connector.loadContactCache();
|
await connector.loadContactCache();
|
||||||
@@ -76,10 +96,35 @@ void main() async {
|
|||||||
bleDebugLogService: bleDebugLogService,
|
bleDebugLogService: bleDebugLogService,
|
||||||
appDebugLogService: appDebugLogService,
|
appDebugLogService: appDebugLogService,
|
||||||
mapTileCacheService: mapTileCacheService,
|
mapTileCacheService: mapTileCacheService,
|
||||||
|
chatTextScaleService: chatTextScaleService,
|
||||||
|
translationService: translationService,
|
||||||
|
uiViewStateService: uiViewStateService,
|
||||||
|
timeoutPredictionService: timeoutPredictionService,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _registerThirdPartyLicenses() {
|
||||||
|
LicenseRegistry.addLicense(() async* {
|
||||||
|
yield const LicenseEntryWithLineBreaks(
|
||||||
|
<String>['Open-Meteo Elevation API Data'],
|
||||||
|
'''
|
||||||
|
Data used by LOS elevation lookups is provided by Open-Meteo.
|
||||||
|
|
||||||
|
Open-Meteo terms and attribution:
|
||||||
|
https://open-meteo.com/en/terms
|
||||||
|
|
||||||
|
Elevation API:
|
||||||
|
https://open-meteo.com/en/docs/elevation-api
|
||||||
|
|
||||||
|
Attribution license reference:
|
||||||
|
Creative Commons Attribution 4.0 International (CC BY 4.0)
|
||||||
|
https://creativecommons.org/licenses/by/4.0/
|
||||||
|
''',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class MeshCoreApp extends StatelessWidget {
|
class MeshCoreApp extends StatelessWidget {
|
||||||
final MeshCoreConnector connector;
|
final MeshCoreConnector connector;
|
||||||
final MessageRetryService retryService;
|
final MessageRetryService retryService;
|
||||||
@@ -89,6 +134,10 @@ class MeshCoreApp extends StatelessWidget {
|
|||||||
final BleDebugLogService bleDebugLogService;
|
final BleDebugLogService bleDebugLogService;
|
||||||
final AppDebugLogService appDebugLogService;
|
final AppDebugLogService appDebugLogService;
|
||||||
final MapTileCacheService mapTileCacheService;
|
final MapTileCacheService mapTileCacheService;
|
||||||
|
final ChatTextScaleService chatTextScaleService;
|
||||||
|
final TranslationService translationService;
|
||||||
|
final UiViewStateService uiViewStateService;
|
||||||
|
final TimeoutPredictionService timeoutPredictionService;
|
||||||
|
|
||||||
const MeshCoreApp({
|
const MeshCoreApp({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -100,6 +149,10 @@ class MeshCoreApp extends StatelessWidget {
|
|||||||
required this.bleDebugLogService,
|
required this.bleDebugLogService,
|
||||||
required this.appDebugLogService,
|
required this.appDebugLogService,
|
||||||
required this.mapTileCacheService,
|
required this.mapTileCacheService,
|
||||||
|
required this.chatTextScaleService,
|
||||||
|
required this.translationService,
|
||||||
|
required this.uiViewStateService,
|
||||||
|
required this.timeoutPredictionService,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -112,8 +165,12 @@ class MeshCoreApp extends StatelessWidget {
|
|||||||
ChangeNotifierProvider.value(value: appSettingsService),
|
ChangeNotifierProvider.value(value: appSettingsService),
|
||||||
ChangeNotifierProvider.value(value: bleDebugLogService),
|
ChangeNotifierProvider.value(value: bleDebugLogService),
|
||||||
ChangeNotifierProvider.value(value: appDebugLogService),
|
ChangeNotifierProvider.value(value: appDebugLogService),
|
||||||
|
ChangeNotifierProvider.value(value: chatTextScaleService),
|
||||||
|
ChangeNotifierProvider.value(value: translationService),
|
||||||
|
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) {
|
||||||
@@ -156,7 +213,9 @@ class MeshCoreApp extends StatelessWidget {
|
|||||||
NotificationService().setLocale(locale);
|
NotificationService().setLocale(locale);
|
||||||
return child ?? const SizedBox.shrink();
|
return child ?? const SizedBox.shrink();
|
||||||
},
|
},
|
||||||
home: const ScannerScreen(),
|
home: (PlatformInfo.isWeb && !PlatformInfo.isChrome)
|
||||||
|
? const ChromeRequiredScreen()
|
||||||
|
: const ScannerScreen(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,3 +1,18 @@
|
|||||||
|
import 'translation_support.dart';
|
||||||
|
|
||||||
|
enum UnitSystem { metric, imperial }
|
||||||
|
|
||||||
|
extension UnitSystemValue on UnitSystem {
|
||||||
|
String get value {
|
||||||
|
switch (this) {
|
||||||
|
case UnitSystem.imperial:
|
||||||
|
return 'imperial';
|
||||||
|
case UnitSystem.metric:
|
||||||
|
return 'metric';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class AppSettings {
|
class AppSettings {
|
||||||
static const Object _unset = Object();
|
static const Object _unset = Object();
|
||||||
|
|
||||||
@@ -5,10 +20,13 @@ 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 Map<String, double>? mapCacheBounds;
|
final Map<String, double>? mapCacheBounds;
|
||||||
final int mapCacheMinZoom;
|
final int mapCacheMinZoom;
|
||||||
final int mapCacheMaxZoom;
|
final int mapCacheMaxZoom;
|
||||||
@@ -17,20 +35,41 @@ 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;
|
||||||
final Map<String, String> batteryChemistryByDeviceId;
|
final Map<String, String> batteryChemistryByDeviceId;
|
||||||
|
final Map<String, String> batteryChemistryByRepeaterId;
|
||||||
|
final UnitSystem unitSystem;
|
||||||
|
final Set<String> mutedChannels;
|
||||||
|
final bool mapShowDiscoveryContacts;
|
||||||
|
final String tcpServerAddress;
|
||||||
|
final int tcpServerPort;
|
||||||
|
final bool jumpToOldestUnread;
|
||||||
|
final bool translationEnabled;
|
||||||
|
final String? translationTargetLanguageCode;
|
||||||
|
final bool composerTranslationEnabled;
|
||||||
|
final String? translationModelSourceUrl;
|
||||||
|
final String? translationSelectedModelId;
|
||||||
|
final List<TranslationModelRecord> translationDownloadedModels;
|
||||||
|
|
||||||
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.mapCacheBounds,
|
this.mapCacheBounds,
|
||||||
this.mapCacheMinZoom = 10,
|
this.mapCacheMinZoom = 10,
|
||||||
this.mapCacheMaxZoom = 15,
|
this.mapCacheMaxZoom = 15,
|
||||||
@@ -39,11 +78,32 @@ 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,
|
||||||
Map<String, String>? batteryChemistryByDeviceId,
|
Map<String, String>? batteryChemistryByDeviceId,
|
||||||
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {};
|
Map<String, String>? batteryChemistryByRepeaterId,
|
||||||
|
this.unitSystem = UnitSystem.metric,
|
||||||
|
Set<String>? mutedChannels,
|
||||||
|
this.mapShowDiscoveryContacts = true,
|
||||||
|
this.tcpServerAddress = '',
|
||||||
|
this.tcpServerPort = 0,
|
||||||
|
this.jumpToOldestUnread = false,
|
||||||
|
this.translationEnabled = false,
|
||||||
|
this.translationTargetLanguageCode,
|
||||||
|
this.composerTranslationEnabled = false,
|
||||||
|
this.translationModelSourceUrl,
|
||||||
|
this.translationSelectedModelId,
|
||||||
|
List<TranslationModelRecord>? translationDownloadedModels,
|
||||||
|
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
|
||||||
|
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
|
||||||
|
mutedChannels = mutedChannels ?? {},
|
||||||
|
translationDownloadedModels = translationDownloadedModels ?? const [];
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
@@ -51,10 +111,13 @@ 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,
|
||||||
'map_cache_bounds': mapCacheBounds,
|
'map_cache_bounds': mapCacheBounds,
|
||||||
'map_cache_min_zoom': mapCacheMinZoom,
|
'map_cache_min_zoom': mapCacheMinZoom,
|
||||||
'map_cache_max_zoom': mapCacheMaxZoom,
|
'map_cache_max_zoom': mapCacheMaxZoom,
|
||||||
@@ -63,24 +126,55 @@ 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,
|
||||||
'battery_chemistry_by_device_id': batteryChemistryByDeviceId,
|
'battery_chemistry_by_device_id': batteryChemistryByDeviceId,
|
||||||
|
'battery_chemistry_by_repeater_id': batteryChemistryByRepeaterId,
|
||||||
|
'unit_system': unitSystem.value,
|
||||||
|
'muted_channels': mutedChannels.toList(),
|
||||||
|
'map_show_discovery_contacts': mapShowDiscoveryContacts,
|
||||||
|
'tcp_server_address': tcpServerAddress,
|
||||||
|
'tcp_server_port': tcpServerPort,
|
||||||
|
'jump_to_oldest_unread': jumpToOldestUnread,
|
||||||
|
'translation_enabled': translationEnabled,
|
||||||
|
'translation_target_language_code': translationTargetLanguageCode,
|
||||||
|
'composer_translation_enabled': composerTranslationEnabled,
|
||||||
|
'translation_model_source_url': translationModelSourceUrl,
|
||||||
|
'translation_selected_model_id': translationSelectedModelId,
|
||||||
|
'translation_downloaded_models': translationDownloadedModels
|
||||||
|
.map((model) => model.toJson())
|
||||||
|
.toList(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
factory AppSettings.fromJson(Map<String, dynamic> json) {
|
factory AppSettings.fromJson(Map<String, dynamic> json) {
|
||||||
|
UnitSystem parseUnitSystem(dynamic value) {
|
||||||
|
if (value is String && value.toLowerCase() == 'imperial') {
|
||||||
|
return UnitSystem.imperial;
|
||||||
|
}
|
||||||
|
return UnitSystem.metric;
|
||||||
|
}
|
||||||
|
|
||||||
return AppSettings(
|
return AppSettings(
|
||||||
clearPathOnMaxRetry: json['clear_path_on_max_retry'] as bool? ?? false,
|
clearPathOnMaxRetry: json['clear_path_on_max_retry'] as bool? ?? false,
|
||||||
mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true,
|
mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true,
|
||||||
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,
|
||||||
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()),
|
||||||
),
|
),
|
||||||
@@ -93,6 +187,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,
|
||||||
@@ -101,6 +203,40 @@ class AppSettings {
|
|||||||
(key, value) => MapEntry(key.toString(), value.toString()),
|
(key, value) => MapEntry(key.toString(), value.toString()),
|
||||||
) ??
|
) ??
|
||||||
{},
|
{},
|
||||||
|
batteryChemistryByRepeaterId:
|
||||||
|
(json['battery_chemistry_by_repeater_id'] as Map?)?.map(
|
||||||
|
(key, value) => MapEntry(key.toString(), value.toString()),
|
||||||
|
) ??
|
||||||
|
{},
|
||||||
|
unitSystem: parseUnitSystem(json['unit_system']),
|
||||||
|
mutedChannels:
|
||||||
|
((json['muted_channels'] as List?)
|
||||||
|
?.map((e) => e.toString())
|
||||||
|
.toSet()) ??
|
||||||
|
{},
|
||||||
|
mapShowDiscoveryContacts:
|
||||||
|
json['map_show_discovery_contacts'] as bool? ?? true,
|
||||||
|
tcpServerAddress: json['tcp_server_address'] as String? ?? '',
|
||||||
|
tcpServerPort: json['tcp_server_port'] as int? ?? 0,
|
||||||
|
jumpToOldestUnread: json['jump_to_oldest_unread'] as bool? ?? false,
|
||||||
|
translationEnabled: json['translation_enabled'] as bool? ?? false,
|
||||||
|
translationTargetLanguageCode:
|
||||||
|
json['translation_target_language_code'] as String?,
|
||||||
|
composerTranslationEnabled:
|
||||||
|
json['composer_translation_enabled'] as bool? ?? false,
|
||||||
|
translationModelSourceUrl:
|
||||||
|
json['translation_model_source_url'] as String?,
|
||||||
|
translationSelectedModelId:
|
||||||
|
json['translation_selected_model_id'] as String?,
|
||||||
|
translationDownloadedModels:
|
||||||
|
(json['translation_downloaded_models'] as List<dynamic>?)
|
||||||
|
?.map(
|
||||||
|
(entry) => TranslationModelRecord.fromJson(
|
||||||
|
Map<String, dynamic>.from(entry as Map),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList() ??
|
||||||
|
const [],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,10 +245,13 @@ 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,
|
||||||
Object? mapCacheBounds = _unset,
|
Object? mapCacheBounds = _unset,
|
||||||
int? mapCacheMinZoom,
|
int? mapCacheMinZoom,
|
||||||
int? mapCacheMaxZoom,
|
int? mapCacheMaxZoom,
|
||||||
@@ -121,20 +260,42 @@ 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,
|
||||||
Map<String, String>? batteryChemistryByDeviceId,
|
Map<String, String>? batteryChemistryByDeviceId,
|
||||||
|
Map<String, String>? batteryChemistryByRepeaterId,
|
||||||
|
UnitSystem? unitSystem,
|
||||||
|
Set<String>? mutedChannels,
|
||||||
|
bool? mapShowDiscoveryContacts,
|
||||||
|
String? tcpServerAddress,
|
||||||
|
int? tcpServerPort,
|
||||||
|
bool? jumpToOldestUnread,
|
||||||
|
bool? translationEnabled,
|
||||||
|
Object? translationTargetLanguageCode = _unset,
|
||||||
|
bool? composerTranslationEnabled,
|
||||||
|
Object? translationModelSourceUrl = _unset,
|
||||||
|
Object? translationSelectedModelId = _unset,
|
||||||
|
List<TranslationModelRecord>? translationDownloadedModels,
|
||||||
}) {
|
}) {
|
||||||
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,
|
||||||
mapCacheBounds: mapCacheBounds == _unset
|
mapCacheBounds: mapCacheBounds == _unset
|
||||||
? this.mapCacheBounds
|
? this.mapCacheBounds
|
||||||
: mapCacheBounds as Map<String, double>?,
|
: mapCacheBounds as Map<String, double>?,
|
||||||
@@ -147,6 +308,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
|
||||||
@@ -154,6 +322,29 @@ class AppSettings {
|
|||||||
appDebugLogEnabled: appDebugLogEnabled ?? this.appDebugLogEnabled,
|
appDebugLogEnabled: appDebugLogEnabled ?? this.appDebugLogEnabled,
|
||||||
batteryChemistryByDeviceId:
|
batteryChemistryByDeviceId:
|
||||||
batteryChemistryByDeviceId ?? this.batteryChemistryByDeviceId,
|
batteryChemistryByDeviceId ?? this.batteryChemistryByDeviceId,
|
||||||
|
batteryChemistryByRepeaterId:
|
||||||
|
batteryChemistryByRepeaterId ?? this.batteryChemistryByRepeaterId,
|
||||||
|
unitSystem: unitSystem ?? this.unitSystem,
|
||||||
|
mutedChannels: mutedChannels ?? this.mutedChannels,
|
||||||
|
mapShowDiscoveryContacts:
|
||||||
|
mapShowDiscoveryContacts ?? this.mapShowDiscoveryContacts,
|
||||||
|
tcpServerAddress: tcpServerAddress ?? this.tcpServerAddress,
|
||||||
|
tcpServerPort: tcpServerPort ?? this.tcpServerPort,
|
||||||
|
jumpToOldestUnread: jumpToOldestUnread ?? this.jumpToOldestUnread,
|
||||||
|
translationEnabled: translationEnabled ?? this.translationEnabled,
|
||||||
|
translationTargetLanguageCode: translationTargetLanguageCode == _unset
|
||||||
|
? this.translationTargetLanguageCode
|
||||||
|
: translationTargetLanguageCode as String?,
|
||||||
|
composerTranslationEnabled:
|
||||||
|
composerTranslationEnabled ?? this.composerTranslationEnabled,
|
||||||
|
translationModelSourceUrl: translationModelSourceUrl == _unset
|
||||||
|
? this.translationModelSourceUrl
|
||||||
|
: translationModelSourceUrl as String?,
|
||||||
|
translationSelectedModelId: translationSelectedModelId == _unset
|
||||||
|
? this.translationSelectedModelId
|
||||||
|
: translationSelectedModelId as String?,
|
||||||
|
translationDownloadedModels:
|
||||||
|
translationDownloadedModels ?? this.translationDownloadedModels,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-9
@@ -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) {
|
||||||
|
|||||||
+114
-79
@@ -2,6 +2,8 @@ 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 'translation_support.dart';
|
||||||
|
import '../utils/app_logger.dart';
|
||||||
|
|
||||||
enum ChannelMessageStatus { pending, sent, failed }
|
enum ChannelMessageStatus { pending, sent, failed }
|
||||||
|
|
||||||
@@ -23,9 +25,16 @@ class Repeat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ChannelMessage {
|
class ChannelMessage {
|
||||||
|
static const Object _unset = Object();
|
||||||
|
|
||||||
final Uint8List? senderKey;
|
final Uint8List? senderKey;
|
||||||
final String senderName;
|
final String senderName;
|
||||||
final String text;
|
final String text;
|
||||||
|
final String? originalText;
|
||||||
|
final String? translatedText;
|
||||||
|
final String? translatedLanguageCode;
|
||||||
|
final MessageTranslationStatus translationStatus;
|
||||||
|
final String? translationModelId;
|
||||||
final DateTime timestamp;
|
final DateTime timestamp;
|
||||||
final bool isOutgoing;
|
final bool isOutgoing;
|
||||||
final ChannelMessageStatus status;
|
final ChannelMessageStatus status;
|
||||||
@@ -36,6 +45,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;
|
||||||
@@ -45,6 +55,11 @@ class ChannelMessage {
|
|||||||
this.senderKey,
|
this.senderKey,
|
||||||
required this.senderName,
|
required this.senderName,
|
||||||
required this.text,
|
required this.text,
|
||||||
|
this.originalText,
|
||||||
|
this.translatedText,
|
||||||
|
this.translatedLanguageCode,
|
||||||
|
this.translationStatus = MessageTranslationStatus.none,
|
||||||
|
this.translationModelId,
|
||||||
required this.timestamp,
|
required this.timestamp,
|
||||||
required this.isOutgoing,
|
required this.isOutgoing,
|
||||||
this.status = ChannelMessageStatus.pending,
|
this.status = ChannelMessageStatus.pending,
|
||||||
@@ -55,6 +70,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,15 +95,34 @@ 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,
|
||||||
|
Object? originalText = _unset,
|
||||||
|
Object? translatedText = _unset,
|
||||||
|
Object? translatedLanguageCode = _unset,
|
||||||
|
MessageTranslationStatus? translationStatus,
|
||||||
|
Object? translationModelId = _unset,
|
||||||
Map<String, int>? reactions,
|
Map<String, int>? reactions,
|
||||||
}) {
|
}) {
|
||||||
return ChannelMessage(
|
return ChannelMessage(
|
||||||
senderKey: senderKey,
|
senderKey: senderKey,
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
text: text,
|
text: text,
|
||||||
|
originalText: originalText == _unset
|
||||||
|
? this.originalText
|
||||||
|
: originalText as String?,
|
||||||
|
translatedText: translatedText == _unset
|
||||||
|
? this.translatedText
|
||||||
|
: translatedText as String?,
|
||||||
|
translatedLanguageCode: translatedLanguageCode == _unset
|
||||||
|
? this.translatedLanguageCode
|
||||||
|
: translatedLanguageCode as String?,
|
||||||
|
translationStatus: translationStatus ?? this.translationStatus,
|
||||||
|
translationModelId: translationModelId == _unset
|
||||||
|
? this.translationModelId
|
||||||
|
: translationModelId as String?,
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
@@ -98,6 +133,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,100 +141,99 @@ 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 reader = BufferReader(frame);
|
||||||
|
final code = reader.readByte();
|
||||||
|
if (code != respCodeChannelMsgRecv && code != respCodeChannelMsgRecvV3) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
final code = data[0];
|
int pathLen;
|
||||||
if (code != respCodeChannelMsgRecv && code != respCodeChannelMsgRecvV3) {
|
int txtType;
|
||||||
|
Uint8List pathBytes = Uint8List(0);
|
||||||
|
int channelIdx;
|
||||||
|
if (code == respCodeChannelMsgRecvV3) {
|
||||||
|
reader.skipBytes(1); // Skip SNR
|
||||||
|
final flags = reader.readByte();
|
||||||
|
final hasPath = (flags & 0x01) != 0;
|
||||||
|
reader.skipBytes(1); // Skip reserved byte
|
||||||
|
channelIdx = reader.readByte();
|
||||||
|
pathLen = reader.readInt8();
|
||||||
|
txtType = reader.readByte();
|
||||||
|
if (hasPath && pathLen > 0) {
|
||||||
|
reader.rewind(); // Rewind to read path length again for pathBytes
|
||||||
|
pathBytes = reader.readBytes(pathLen);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channelIdx = reader.readByte();
|
||||||
|
pathLen = reader.readInt8();
|
||||||
|
txtType = reader.readByte();
|
||||||
|
}
|
||||||
|
final timestampRaw = reader.readUInt32LE();
|
||||||
|
|
||||||
|
if (txtType != txtTypePlain) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final text = reader.readCString();
|
||||||
|
|
||||||
|
// Extract sender name and actual message from "name: msg" format
|
||||||
|
String senderName = 'Unknown';
|
||||||
|
String actualText = text;
|
||||||
|
|
||||||
|
final colonIndex = text.indexOf(':');
|
||||||
|
if (colonIndex > 0 && colonIndex < text.length - 1 && colonIndex < 50) {
|
||||||
|
final potentialSender = text.substring(0, colonIndex);
|
||||||
|
if (!RegExp(r'[:\[\]]').hasMatch(potentialSender)) {
|
||||||
|
senderName = potentialSender;
|
||||||
|
final offset =
|
||||||
|
(colonIndex + 1 < text.length && text[colonIndex + 1] == ' ')
|
||||||
|
? colonIndex + 2
|
||||||
|
: colonIndex + 1;
|
||||||
|
actualText = text.substring(offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final decodedText = Smaz.tryDecodePrefixed(actualText) ?? actualText;
|
||||||
|
|
||||||
|
return ChannelMessage(
|
||||||
|
senderKey: null,
|
||||||
|
senderName: senderName,
|
||||||
|
text: decodedText,
|
||||||
|
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
|
||||||
|
isOutgoing: false,
|
||||||
|
status: ChannelMessageStatus.sent,
|
||||||
|
pathLength: pathLen,
|
||||||
|
pathBytes: pathBytes,
|
||||||
|
channelIndex: channelIdx,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
appLogger.error('Error parsing channel message frame: $e');
|
||||||
|
// If parsing fails, return null to avoid crashes
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
int timestampOffset, textOffset, pathLenOffset, txtTypeOffset;
|
|
||||||
Uint8List pathBytes = Uint8List(0);
|
|
||||||
int channelIdx;
|
|
||||||
|
|
||||||
if (code == respCodeChannelMsgRecvV3) {
|
|
||||||
channelIdx = data[4];
|
|
||||||
pathLenOffset = 5;
|
|
||||||
final pathLen = data[pathLenOffset].toSigned(8);
|
|
||||||
var cursor = 6;
|
|
||||||
final hasPathBytesFlag = (data[2] & 0x01) != 0;
|
|
||||||
final canFitPath = pathLen > 0 && data.length >= cursor + pathLen + 5;
|
|
||||||
final hasValidTxtType =
|
|
||||||
cursor < data.length &&
|
|
||||||
(data[cursor] == txtTypePlain || data[cursor] == txtTypeCliData);
|
|
||||||
if ((hasPathBytesFlag || (canFitPath && !hasValidTxtType)) &&
|
|
||||||
canFitPath) {
|
|
||||||
pathBytes = Uint8List.fromList(data.sublist(cursor, cursor + pathLen));
|
|
||||||
cursor += pathLen;
|
|
||||||
}
|
|
||||||
txtTypeOffset = cursor;
|
|
||||||
cursor += 1; // txt_type
|
|
||||||
timestampOffset = cursor;
|
|
||||||
textOffset = cursor + 4;
|
|
||||||
} else {
|
|
||||||
channelIdx = data[1];
|
|
||||||
pathLenOffset = 2;
|
|
||||||
txtTypeOffset = 3;
|
|
||||||
timestampOffset = 4;
|
|
||||||
textOffset = 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.length < textOffset + 1) return null;
|
|
||||||
|
|
||||||
final txtType = data[txtTypeOffset];
|
|
||||||
if (txtType != txtTypePlain) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final pathLen = data[pathLenOffset].toSigned(8);
|
|
||||||
final timestampRaw = readUint32LE(data, timestampOffset);
|
|
||||||
final text = readCString(data, textOffset, data.length - textOffset);
|
|
||||||
|
|
||||||
// Extract sender name and actual message from "name: msg" format
|
|
||||||
String senderName = 'Unknown';
|
|
||||||
String actualText = text;
|
|
||||||
|
|
||||||
final colonIndex = text.indexOf(':');
|
|
||||||
if (colonIndex > 0 && colonIndex < text.length - 1 && colonIndex < 50) {
|
|
||||||
final potentialSender = text.substring(0, colonIndex);
|
|
||||||
if (!RegExp(r'[:\[\]]').hasMatch(potentialSender)) {
|
|
||||||
senderName = potentialSender;
|
|
||||||
final offset =
|
|
||||||
(colonIndex + 1 < text.length && text[colonIndex + 1] == ' ')
|
|
||||||
? colonIndex + 2
|
|
||||||
: colonIndex + 1;
|
|
||||||
actualText = text.substring(offset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final decodedText = Smaz.tryDecodePrefixed(actualText) ?? actualText;
|
|
||||||
|
|
||||||
return ChannelMessage(
|
|
||||||
senderKey: null,
|
|
||||||
senderName: senderName,
|
|
||||||
text: decodedText,
|
|
||||||
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
|
|
||||||
isOutgoing: false,
|
|
||||||
status: ChannelMessageStatus.sent,
|
|
||||||
pathLength: pathLen,
|
|
||||||
pathBytes: pathBytes,
|
|
||||||
channelIndex: channelIdx,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static ChannelMessage outgoing(
|
static ChannelMessage outgoing(
|
||||||
String text,
|
String text,
|
||||||
String senderName,
|
String senderName,
|
||||||
int channelIndex,
|
int channelIndex, {
|
||||||
) {
|
String? originalText,
|
||||||
|
String? translatedLanguageCode,
|
||||||
|
String? translationModelId,
|
||||||
|
}) {
|
||||||
return ChannelMessage(
|
return ChannelMessage(
|
||||||
senderKey: null,
|
senderKey: null,
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
text: text,
|
text: text,
|
||||||
|
originalText: originalText,
|
||||||
|
translatedLanguageCode: translatedLanguageCode,
|
||||||
|
translationModelId: translationModelId,
|
||||||
timestamp: DateTime.now(),
|
timestamp: DateTime.now(),
|
||||||
isOutgoing: true,
|
isOutgoing: true,
|
||||||
status: ChannelMessageStatus.pending,
|
status: ChannelMessageStatus.pending,
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import '../connector/meshcore_protocol.dart';
|
||||||
|
import '../utils/app_logger.dart';
|
||||||
|
|
||||||
|
/// Parsed `RESP_CODE_STATS` + `STATS_TYPE_RADIO` (14 bytes total).
|
||||||
|
class CompanionRadioStats {
|
||||||
|
final int noiseFloorDbm;
|
||||||
|
final int lastRssiDbm;
|
||||||
|
final double lastSnrDb;
|
||||||
|
final int txAirSecs;
|
||||||
|
final int rxAirSecs;
|
||||||
|
final DateTime receivedAt;
|
||||||
|
|
||||||
|
const CompanionRadioStats({
|
||||||
|
required this.noiseFloorDbm,
|
||||||
|
required this.lastRssiDbm,
|
||||||
|
required this.lastSnrDb,
|
||||||
|
required this.txAirSecs,
|
||||||
|
required this.rxAirSecs,
|
||||||
|
required this.receivedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
static CompanionRadioStats? tryParse(Uint8List frame) {
|
||||||
|
if (frame.length < 14) return null;
|
||||||
|
if (frame[0] != respCodeStats || frame[1] != statsTypeRadio) return null;
|
||||||
|
try {
|
||||||
|
final reader = BufferReader(frame);
|
||||||
|
reader.skipBytes(2);
|
||||||
|
final noise = reader.readInt16LE();
|
||||||
|
final rssi = reader.readInt8();
|
||||||
|
final snrRaw = reader.readInt8();
|
||||||
|
final txAir = reader.readUInt32LE();
|
||||||
|
final rxAir = reader.readUInt32LE();
|
||||||
|
return CompanionRadioStats(
|
||||||
|
noiseFloorDbm: noise,
|
||||||
|
lastRssiDbm: rssi,
|
||||||
|
lastSnrDb: snrRaw / 4.0,
|
||||||
|
txAirSecs: txAir,
|
||||||
|
rxAirSecs: rxAir,
|
||||||
|
receivedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
appLogger.warn('CompanionRadioStats parse error: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+96
-79
@@ -1,10 +1,13 @@
|
|||||||
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 {
|
||||||
final Uint8List publicKey;
|
final Uint8List publicKey;
|
||||||
final String name;
|
final String name;
|
||||||
final int type;
|
final int type;
|
||||||
|
final int flags;
|
||||||
final int pathLength; // -1 = flood, 0+ = direct hops (from device)
|
final int pathLength; // -1 = flood, 0+ = direct hops (from device)
|
||||||
final Uint8List path; // Path bytes from device
|
final Uint8List path; // Path bytes from device
|
||||||
final int?
|
final int?
|
||||||
@@ -14,11 +17,15 @@ 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,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.type,
|
required this.type,
|
||||||
|
this.flags = 0,
|
||||||
required this.pathLength,
|
required this.pathLength,
|
||||||
required this.path,
|
required this.path,
|
||||||
this.pathOverride,
|
this.pathOverride,
|
||||||
@@ -27,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);
|
||||||
@@ -57,12 +67,24 @@ 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;
|
||||||
|
|
||||||
Contact copyWith({
|
Contact copyWith({
|
||||||
Uint8List? publicKey,
|
Uint8List? publicKey,
|
||||||
String? name,
|
String? name,
|
||||||
int? type,
|
int? type,
|
||||||
|
int? flags,
|
||||||
int? pathLength,
|
int? pathLength,
|
||||||
Uint8List? path,
|
Uint8List? path,
|
||||||
int? pathOverride,
|
int? pathOverride,
|
||||||
@@ -72,11 +94,14 @@ 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,
|
||||||
name: name ?? this.name,
|
name: name ?? this.name,
|
||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
|
flags: flags ?? this.flags,
|
||||||
pathLength: pathLength ?? this.pathLength,
|
pathLength: pathLength ?? this.pathLength,
|
||||||
path: path ?? this.path,
|
path: path ?? this.path,
|
||||||
pathOverride: clearPathOverride
|
pathOverride: clearPathOverride
|
||||||
@@ -89,18 +114,19 @@ 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 {
|
/// Formats path bytes into comma-separated hex groups of [hashByteWidth] bytes.
|
||||||
final pathBytes = _pathBytesForDisplay;
|
String pathFormattedIdList(int hashByteWidth) {
|
||||||
|
final pathBytes = pathBytesForDisplay;
|
||||||
if (pathBytes.isEmpty) return '';
|
if (pathBytes.isEmpty) return '';
|
||||||
|
final w = hashByteWidth.clamp(1, 8);
|
||||||
final parts = <String>[];
|
final parts = <String>[];
|
||||||
final groupSize = pathHashSize;
|
for (int i = 0; i < pathBytes.length; i += w) {
|
||||||
for (int i = 0; i < pathBytes.length; i += groupSize) {
|
final end = (i + w) <= pathBytes.length ? (i + w) : pathBytes.length;
|
||||||
final end = (i + groupSize) <= pathBytes.length
|
|
||||||
? (i + groupSize)
|
|
||||||
: pathBytes.length;
|
|
||||||
final chunk = pathBytes.sublist(i, end);
|
final chunk = pathBytes.sublist(i, end);
|
||||||
parts.add(
|
parts.add(
|
||||||
chunk
|
chunk
|
||||||
@@ -111,47 +137,14 @@ class Contact {
|
|||||||
return parts.join(',');
|
return parts.join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Default grouping uses legacy single-byte hop hash width.
|
||||||
|
String get pathIdList => pathFormattedIdList(pathHashSize);
|
||||||
|
|
||||||
String get shortPubKeyHex {
|
String get shortPubKeyHex {
|
||||||
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 (pathLength <= 0) {
|
|
||||||
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);
|
||||||
@@ -160,43 +153,64 @@ class Contact {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Contact? fromFrame(Uint8List data) {
|
static Contact? fromFrame(Uint8List data) {
|
||||||
if (data.length < contactFrameSize) return null;
|
if (data.isEmpty) return null;
|
||||||
if (data[0] != respCodeContact) return null;
|
final reader = BufferReader(data);
|
||||||
|
try {
|
||||||
|
final respCode = reader.readByte();
|
||||||
|
if (respCode != respCodeContact && respCode != pushCodeNewAdvert) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final pubKey = reader.readBytes(pubKeySize);
|
||||||
|
|
||||||
final pubKey = Uint8List.fromList(
|
// Guard: reject contacts with zeroed or mostly-zeroed public keys
|
||||||
data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize),
|
// (indicates corrupt flash storage on the firmware side)
|
||||||
);
|
final zeroCount = pubKey.where((b) => b == 0).length;
|
||||||
final type = data[contactTypeOffset];
|
if (zeroCount > pubKeySize ~/ 2) return null;
|
||||||
final pathLen = data[contactPathLenOffset].toSigned(8);
|
|
||||||
final safePathLen = pathLen > 0
|
|
||||||
? (pathLen > maxPathSize ? maxPathSize : pathLen)
|
|
||||||
: 0;
|
|
||||||
final pathBytes = safePathLen > 0
|
|
||||||
? Uint8List.fromList(
|
|
||||||
data.sublist(contactPathOffset, contactPathOffset + safePathLen),
|
|
||||||
)
|
|
||||||
: Uint8List(0);
|
|
||||||
final name = readCString(data, contactNameOffset, maxNameSize);
|
|
||||||
final lastmod = readUint32LE(data, contactLastmodOffset);
|
|
||||||
|
|
||||||
double? lat, lon;
|
final type = reader.readByte();
|
||||||
final latRaw = readInt32LE(data, contactLatOffset);
|
final flags = reader.readByte();
|
||||||
final lonRaw = readInt32LE(data, contactLonOffset);
|
final pathLen = reader.readByte();
|
||||||
if (latRaw != 0 || lonRaw != 0) {
|
final safePathLen = pathLen > 0
|
||||||
lat = latRaw / 1e6;
|
? (pathLen > maxPathSize ? maxPathSize : pathLen)
|
||||||
lon = lonRaw / 1e6;
|
: 0;
|
||||||
|
final pathBytes = reader.readBytes(maxPathSize).sublist(0, safePathLen);
|
||||||
|
final name = reader.readCStringGreedy(maxNameSize);
|
||||||
|
|
||||||
|
// Guard: reject contacts with non-printable names (corrupt flash data)
|
||||||
|
if (name.isNotEmpty &&
|
||||||
|
name.codeUnits.every((c) => c < 0x20 || c == 0xFFFD)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final lastMod = reader.readUInt32LE();
|
||||||
|
|
||||||
|
double? lat, lon;
|
||||||
|
if (reader.remaining >= 8) {
|
||||||
|
final latRaw = reader.readInt32LE();
|
||||||
|
final lonRaw = reader.readInt32LE();
|
||||||
|
if (latRaw != 0 || lonRaw != 0) {
|
||||||
|
lat = latRaw / 1e6;
|
||||||
|
lon = lonRaw / 1e6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Contact(
|
||||||
|
publicKey: pubKey,
|
||||||
|
name: name.isEmpty ? 'Unknown' : name,
|
||||||
|
type: type,
|
||||||
|
flags: flags,
|
||||||
|
pathLength: (pathLen == 0xFF || pathLen > maxPathSize) ? -1 : pathLen,
|
||||||
|
path: pathBytes,
|
||||||
|
latitude: lat,
|
||||||
|
longitude: lon,
|
||||||
|
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastMod * 1000),
|
||||||
|
isActive: true,
|
||||||
|
rawPacket: null,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
appLogger.error('Failed to parse contact frame: $e');
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Contact(
|
|
||||||
publicKey: pubKey,
|
|
||||||
name: name.isEmpty ? 'Unknown' : name,
|
|
||||||
type: type,
|
|
||||||
pathLength: pathLen,
|
|
||||||
path: pathBytes,
|
|
||||||
latitude: lat,
|
|
||||||
longitude: lon,
|
|
||||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -205,4 +219,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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+77
-30
@@ -1,28 +1,37 @@
|
|||||||
import 'dart:typed_data';
|
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 'translation_support.dart';
|
||||||
|
|
||||||
enum MessageStatus { pending, sent, delivered, failed }
|
enum MessageStatus { pending, sent, delivered, failed }
|
||||||
|
|
||||||
class Message {
|
class Message {
|
||||||
|
static const Object _unset = Object();
|
||||||
|
|
||||||
final Uint8List senderKey;
|
final Uint8List senderKey;
|
||||||
final String text;
|
final String text;
|
||||||
final DateTime timestamp;
|
final DateTime timestamp;
|
||||||
final bool isOutgoing;
|
final bool isOutgoing;
|
||||||
final bool isCli;
|
final bool isCli;
|
||||||
final MessageStatus status;
|
final MessageStatus status;
|
||||||
|
final String? originalText;
|
||||||
|
final String? translatedText;
|
||||||
|
final String? translatedLanguageCode;
|
||||||
|
final MessageTranslationStatus translationStatus;
|
||||||
|
final String? translationModelId;
|
||||||
|
|
||||||
// NEW: Retry logic fields
|
// NEW: Retry logic fields
|
||||||
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({
|
||||||
@@ -32,7 +41,12 @@ class Message {
|
|||||||
required this.isOutgoing,
|
required this.isOutgoing,
|
||||||
this.isCli = false,
|
this.isCli = false,
|
||||||
this.status = MessageStatus.pending,
|
this.status = MessageStatus.pending,
|
||||||
this.messageId,
|
String? messageId,
|
||||||
|
this.originalText,
|
||||||
|
this.translatedText,
|
||||||
|
this.translatedLanguageCode,
|
||||||
|
this.translationStatus = MessageTranslationStatus.none,
|
||||||
|
this.translationModelId,
|
||||||
this.retryCount = 0,
|
this.retryCount = 0,
|
||||||
this.estimatedTimeoutMs,
|
this.estimatedTimeoutMs,
|
||||||
this.expectedAckHash,
|
this.expectedAckHash,
|
||||||
@@ -43,9 +57,14 @@ class Message {
|
|||||||
Uint8List? pathBytes,
|
Uint8List? pathBytes,
|
||||||
Uint8List? fourByteRoomContactKey,
|
Uint8List? fourByteRoomContactKey,
|
||||||
Map<String, int>? reactions,
|
Map<String, int>? reactions,
|
||||||
}) : pathBytes = pathBytes ?? Uint8List(0),
|
Map<String, MessageStatus>? reactionStatuses,
|
||||||
|
}) : messageId =
|
||||||
|
messageId ??
|
||||||
|
'${timestamp.millisecondsSinceEpoch}_${pubKeyToHex(senderKey)}_${text.hashCode}',
|
||||||
|
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,14 +72,20 @@ 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,
|
||||||
int? pathLength,
|
int? pathLength,
|
||||||
Uint8List? pathBytes,
|
Uint8List? pathBytes,
|
||||||
bool? isCli,
|
bool? isCli,
|
||||||
|
Object? originalText = _unset,
|
||||||
|
Object? translatedText = _unset,
|
||||||
|
Object? translatedLanguageCode = _unset,
|
||||||
|
MessageTranslationStatus? translationStatus,
|
||||||
|
Object? translationModelId = _unset,
|
||||||
Map<String, int>? reactions,
|
Map<String, int>? reactions,
|
||||||
|
Map<String, MessageStatus>? reactionStatuses,
|
||||||
Uint8List? fourByteRoomContactKey,
|
Uint8List? fourByteRoomContactKey,
|
||||||
}) {
|
}) {
|
||||||
return Message(
|
return Message(
|
||||||
@@ -71,6 +96,19 @@ class Message {
|
|||||||
isCli: isCli ?? this.isCli,
|
isCli: isCli ?? this.isCli,
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
messageId: messageId,
|
messageId: messageId,
|
||||||
|
originalText: originalText == _unset
|
||||||
|
? this.originalText
|
||||||
|
: originalText as String?,
|
||||||
|
translatedText: translatedText == _unset
|
||||||
|
? this.translatedText
|
||||||
|
: translatedText as String?,
|
||||||
|
translatedLanguageCode: translatedLanguageCode == _unset
|
||||||
|
? this.translatedLanguageCode
|
||||||
|
: translatedLanguageCode as String?,
|
||||||
|
translationStatus: translationStatus ?? this.translationStatus,
|
||||||
|
translationModelId: translationModelId == _unset
|
||||||
|
? this.translationModelId
|
||||||
|
: translationModelId as String?,
|
||||||
retryCount: retryCount ?? this.retryCount,
|
retryCount: retryCount ?? this.retryCount,
|
||||||
estimatedTimeoutMs: estimatedTimeoutMs ?? this.estimatedTimeoutMs,
|
estimatedTimeoutMs: estimatedTimeoutMs ?? this.estimatedTimeoutMs,
|
||||||
expectedAckHash: expectedAckHash ?? this.expectedAckHash,
|
expectedAckHash: expectedAckHash ?? this.expectedAckHash,
|
||||||
@@ -80,49 +118,58 @@ 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);
|
||||||
|
try {
|
||||||
|
final code = reader.readByte();
|
||||||
|
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
final code = data[0];
|
final senderKey = reader.readBytes(pubKeySize);
|
||||||
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
|
final timestampRaw = reader.readInt32LE();
|
||||||
|
final flags = reader.readByte();
|
||||||
|
if ((flags >> 2) != txtTypePlain) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final text = reader.readCString();
|
||||||
|
|
||||||
|
return Message(
|
||||||
|
senderKey: senderKey,
|
||||||
|
text: text,
|
||||||
|
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
|
||||||
|
isOutgoing: false,
|
||||||
|
isCli: false,
|
||||||
|
status: MessageStatus.delivered,
|
||||||
|
pathBytes: Uint8List(0),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final senderKey = Uint8List.fromList(
|
|
||||||
data.sublist(msgPubKeyOffset, msgPubKeyOffset + pubKeySize),
|
|
||||||
);
|
|
||||||
final timestampRaw = readUint32LE(data, msgTimestampOffset);
|
|
||||||
final flags = data[msgFlagsOffset];
|
|
||||||
if ((flags >> 2) != txtTypePlain) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final text = readCString(data, msgTextOffset, data.length - msgTextOffset);
|
|
||||||
|
|
||||||
return Message(
|
|
||||||
senderKey: senderKey,
|
|
||||||
text: text,
|
|
||||||
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
|
|
||||||
isOutgoing: false,
|
|
||||||
isCli: false,
|
|
||||||
status: MessageStatus.delivered,
|
|
||||||
pathBytes: Uint8List(0),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Message outgoing(
|
static Message outgoing(
|
||||||
Uint8List recipientKey,
|
Uint8List recipientKey,
|
||||||
String text, {
|
String text, {
|
||||||
|
String? originalText,
|
||||||
|
String? translatedLanguageCode,
|
||||||
|
String? translationModelId,
|
||||||
int? pathLength,
|
int? pathLength,
|
||||||
Uint8List? pathBytes,
|
Uint8List? pathBytes,
|
||||||
}) {
|
}) {
|
||||||
return Message(
|
return Message(
|
||||||
senderKey: recipientKey,
|
senderKey: recipientKey,
|
||||||
text: text,
|
text: text,
|
||||||
|
originalText: originalText,
|
||||||
|
translatedLanguageCode: translatedLanguageCode,
|
||||||
|
translationModelId: translationModelId,
|
||||||
timestamp: DateTime.now(),
|
timestamp: DateTime.now(),
|
||||||
isOutgoing: true,
|
isOutgoing: true,
|
||||||
isCli: false,
|
isCli: false,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
enum MessageTranslationStatus { none, pending, completed, failed, skipped }
|
||||||
|
|
||||||
|
extension MessageTranslationStatusValue on MessageTranslationStatus {
|
||||||
|
String get value {
|
||||||
|
switch (this) {
|
||||||
|
case MessageTranslationStatus.pending:
|
||||||
|
return 'pending';
|
||||||
|
case MessageTranslationStatus.completed:
|
||||||
|
return 'completed';
|
||||||
|
case MessageTranslationStatus.failed:
|
||||||
|
return 'failed';
|
||||||
|
case MessageTranslationStatus.skipped:
|
||||||
|
return 'skipped';
|
||||||
|
case MessageTranslationStatus.none:
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageTranslationStatus parseMessageTranslationStatus(dynamic value) {
|
||||||
|
if (value is! String) {
|
||||||
|
return MessageTranslationStatus.none;
|
||||||
|
}
|
||||||
|
for (final status in MessageTranslationStatus.values) {
|
||||||
|
if (status.value == value) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MessageTranslationStatus.none;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TranslationModelRecord {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String sourceUrl;
|
||||||
|
final String localPath;
|
||||||
|
final DateTime downloadedAt;
|
||||||
|
final int fileSizeBytes;
|
||||||
|
|
||||||
|
const TranslationModelRecord({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.sourceUrl,
|
||||||
|
required this.localPath,
|
||||||
|
required this.downloadedAt,
|
||||||
|
required this.fileSizeBytes,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'source_url': sourceUrl,
|
||||||
|
'local_path': localPath,
|
||||||
|
'downloaded_at': downloadedAt.millisecondsSinceEpoch,
|
||||||
|
'file_size_bytes': fileSizeBytes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory TranslationModelRecord.fromJson(Map<String, dynamic> json) {
|
||||||
|
return TranslationModelRecord(
|
||||||
|
id: json['id'] as String? ?? '',
|
||||||
|
name: json['name'] as String? ?? '',
|
||||||
|
sourceUrl: json['source_url'] as String? ?? '',
|
||||||
|
localPath: json['local_path'] as String? ?? '',
|
||||||
|
downloadedAt: DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
json['downloaded_at'] as int? ?? 0,
|
||||||
|
),
|
||||||
|
fileSizeBytes: json['file_size_bytes'] as int? ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String translationModelFriendlyName(TranslationModelRecord model) {
|
||||||
|
switch (model.id) {
|
||||||
|
case 'hy-mt1.5-1.8b-q4_k_m':
|
||||||
|
return 'Tencent HY-MT 1.5 1.8B Q4_K_M';
|
||||||
|
case 'hy-mt1.5-1.8b-q6_k':
|
||||||
|
return 'Tencent HY-MT 1.5 1.8B Q6_K';
|
||||||
|
default:
|
||||||
|
final trimmed = model.name.trim();
|
||||||
|
if (trimmed.endsWith('.gguf')) {
|
||||||
|
return trimmed.substring(0, trimmed.length - 5);
|
||||||
|
}
|
||||||
|
return trimmed.isEmpty ? model.id : trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TranslationLanguageOption {
|
||||||
|
final String code;
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
const TranslationLanguageOption({required this.code, required this.label});
|
||||||
|
}
|
||||||
|
|
||||||
|
const List<TranslationLanguageOption> supportedTranslationLanguages = [
|
||||||
|
TranslationLanguageOption(code: 'bg', label: 'Bulgarian'),
|
||||||
|
TranslationLanguageOption(code: 'de', label: 'German'),
|
||||||
|
TranslationLanguageOption(code: 'en', label: 'English'),
|
||||||
|
TranslationLanguageOption(code: 'es', label: 'Spanish'),
|
||||||
|
TranslationLanguageOption(code: 'fr', label: 'French'),
|
||||||
|
TranslationLanguageOption(code: 'hu', label: 'Hungarian'),
|
||||||
|
TranslationLanguageOption(code: 'it', label: 'Italian'),
|
||||||
|
TranslationLanguageOption(code: 'ja', label: 'Japanese'),
|
||||||
|
TranslationLanguageOption(code: 'ko', label: 'Korean'),
|
||||||
|
TranslationLanguageOption(code: 'nl', label: 'Dutch'),
|
||||||
|
TranslationLanguageOption(code: 'pl', label: 'Polish'),
|
||||||
|
TranslationLanguageOption(code: 'pt', label: 'Portuguese'),
|
||||||
|
TranslationLanguageOption(code: 'ru', label: 'Russian'),
|
||||||
|
TranslationLanguageOption(code: 'sk', label: 'Slovak'),
|
||||||
|
TranslationLanguageOption(code: 'sl', label: 'Slovenian'),
|
||||||
|
TranslationLanguageOption(code: 'sv', label: 'Swedish'),
|
||||||
|
TranslationLanguageOption(code: 'uk', label: 'Ukrainian'),
|
||||||
|
TranslationLanguageOption(code: 'zh', label: 'Chinese'),
|
||||||
|
];
|
||||||
|
|
||||||
|
final List<TranslationModelRecord> translationPresetModels = [
|
||||||
|
TranslationModelRecord(
|
||||||
|
id: 'hy-mt1.5-1.8b-q4_k_m',
|
||||||
|
name: 'HY-MT1.5-1.8B-Q4_K_M.gguf',
|
||||||
|
sourceUrl:
|
||||||
|
'https://huggingface.co/tencent/HY-MT1.5-1.8B-GGUF/resolve/main/HY-MT1.5-1.8B-Q4_K_M.gguf?download=true',
|
||||||
|
localPath: '',
|
||||||
|
downloadedAt: DateTime.fromMillisecondsSinceEpoch(0),
|
||||||
|
fileSizeBytes: 0,
|
||||||
|
),
|
||||||
|
TranslationModelRecord(
|
||||||
|
id: 'hy-mt1.5-1.8b-q6_k',
|
||||||
|
name: 'HY-MT1.5-1.8B-Q6_K.gguf',
|
||||||
|
sourceUrl:
|
||||||
|
'https://huggingface.co/tencent/HY-MT1.5-1.8B-GGUF/resolve/main/HY-MT1.5-1.8B-Q6_K.gguf?download=true',
|
||||||
|
localPath: '',
|
||||||
|
downloadedAt: DateTime.fromMillisecondsSinceEpoch(0),
|
||||||
|
fileSizeBytes: 0,
|
||||||
|
),
|
||||||
|
];
|
||||||
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../services/app_debug_log_service.dart';
|
import '../services/app_debug_log_service.dart';
|
||||||
|
import '../widgets/adaptive_app_bar_title.dart';
|
||||||
|
|
||||||
class AppDebugLogScreen extends StatelessWidget {
|
class AppDebugLogScreen extends StatelessWidget {
|
||||||
const AppDebugLogScreen({super.key});
|
const AppDebugLogScreen({super.key});
|
||||||
@@ -17,7 +18,7 @@ class AppDebugLogScreen extends StatelessWidget {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(context.l10n.debugLog_appTitle),
|
title: AdaptiveAppBarTitle(context.l10n.debugLog_appTitle),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
|
import '../models/app_settings.dart';
|
||||||
|
import '../models/translation_support.dart';
|
||||||
import '../services/app_settings_service.dart';
|
import '../services/app_settings_service.dart';
|
||||||
import '../services/notification_service.dart';
|
import '../services/notification_service.dart';
|
||||||
|
import '../services/translation_service.dart';
|
||||||
|
import '../widgets/adaptive_app_bar_title.dart';
|
||||||
import 'map_cache_screen.dart';
|
import 'map_cache_screen.dart';
|
||||||
|
|
||||||
class AppSettingsScreen extends StatelessWidget {
|
class AppSettingsScreen extends StatelessWidget {
|
||||||
@@ -14,31 +19,51 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(context.l10n.appSettings_title),
|
title: AdaptiveAppBarTitle(context.l10n.appSettings_title),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
child: Consumer2<AppSettingsService, MeshCoreConnector>(
|
child:
|
||||||
builder: (context, settingsService, connector, child) {
|
Consumer3<
|
||||||
return ListView(
|
AppSettingsService,
|
||||||
padding: const EdgeInsets.all(16),
|
MeshCoreConnector,
|
||||||
children: [
|
TranslationService
|
||||||
_buildAppearanceCard(context, settingsService),
|
>(
|
||||||
const SizedBox(height: 16),
|
builder:
|
||||||
_buildNotificationsCard(context, settingsService),
|
(
|
||||||
const SizedBox(height: 16),
|
context,
|
||||||
_buildMessagingCard(context, settingsService),
|
settingsService,
|
||||||
const SizedBox(height: 16),
|
connector,
|
||||||
_buildBatteryCard(context, settingsService, connector),
|
translationService,
|
||||||
const SizedBox(height: 16),
|
child,
|
||||||
_buildMapSettingsCard(context, settingsService),
|
) {
|
||||||
const SizedBox(height: 16),
|
return ListView(
|
||||||
_buildDebugCard(context, settingsService),
|
padding: const EdgeInsets.all(16),
|
||||||
],
|
children: [
|
||||||
);
|
_buildAppearanceCard(context, settingsService),
|
||||||
},
|
const SizedBox(height: 16),
|
||||||
),
|
_buildNotificationsCard(context, settingsService),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildMessagingCard(context, settingsService),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
if (!kIsWeb) ...[
|
||||||
|
_buildTranslationCard(
|
||||||
|
context,
|
||||||
|
settingsService,
|
||||||
|
translationService,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
_buildBatteryCard(context, settingsService, connector),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildMapSettingsCard(context, settingsService),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildDebugCard(context, settingsService),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -80,6 +105,18 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
onTap: () => _showLanguageDialog(context, settingsService),
|
onTap: () => _showLanguageDialog(context, settingsService),
|
||||||
),
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
SwitchListTile(
|
||||||
|
secondary: const Icon(Icons.location_searching),
|
||||||
|
title: Text(context.l10n.appSettings_enableMessageTracing),
|
||||||
|
subtitle: Text(
|
||||||
|
context.l10n.appSettings_enableMessageTracingSubtitle,
|
||||||
|
),
|
||||||
|
value: settingsService.settings.enableMessageTracing,
|
||||||
|
onChanged: (value) {
|
||||||
|
settingsService.setEnableMessageTracing(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -277,6 +314,14 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
|
SwitchListTile(
|
||||||
|
secondary: const Icon(Icons.vertical_align_top),
|
||||||
|
title: Text(context.l10n.appSettings_jumpToOldestUnread),
|
||||||
|
subtitle: Text(context.l10n.appSettings_jumpToOldestUnreadSubtitle),
|
||||||
|
value: settingsService.settings.jumpToOldestUnread,
|
||||||
|
onChanged: settingsService.setJumpToOldestUnread,
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
secondary: const Icon(Icons.alt_route),
|
secondary: const Icon(Icons.alt_route),
|
||||||
title: Text(context.l10n.appSettings_autoRouteRotation),
|
title: Text(context.l10n.appSettings_autoRouteRotation),
|
||||||
@@ -296,6 +341,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()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -360,6 +517,18 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
onTap: () => _showTimeFilterDialog(context, settingsService),
|
onTap: () => _showTimeFilterDialog(context, settingsService),
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.straighten),
|
||||||
|
title: Text(context.l10n.appSettings_unitsTitle),
|
||||||
|
subtitle: Text(
|
||||||
|
settingsService.settings.unitSystem == UnitSystem.imperial
|
||||||
|
? context.l10n.appSettings_unitsImperial
|
||||||
|
: context.l10n.appSettings_unitsMetric,
|
||||||
|
),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () => _showUnitsDialog(context, settingsService),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.download_outlined),
|
leading: const Icon(Icons.download_outlined),
|
||||||
title: Text(context.l10n.appSettings_offlineMapCache),
|
title: Text(context.l10n.appSettings_offlineMapCache),
|
||||||
@@ -384,6 +553,211 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildTranslationCard(
|
||||||
|
BuildContext context,
|
||||||
|
AppSettingsService settingsService,
|
||||||
|
TranslationService translationService,
|
||||||
|
) {
|
||||||
|
final settings = settingsService.settings;
|
||||||
|
return Card(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||||
|
child: Text(
|
||||||
|
context.l10n.translation_title,
|
||||||
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SwitchListTile(
|
||||||
|
secondary: const Icon(Icons.translate),
|
||||||
|
title: Text(context.l10n.translation_enableTitle),
|
||||||
|
subtitle: Text(context.l10n.translation_enableSubtitle),
|
||||||
|
value: settings.translationEnabled,
|
||||||
|
onChanged: settingsService.setTranslationEnabled,
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
SwitchListTile(
|
||||||
|
secondary: const Icon(Icons.outgoing_mail),
|
||||||
|
title: Text(context.l10n.translation_composerTitle),
|
||||||
|
subtitle: Text(context.l10n.translation_composerSubtitle),
|
||||||
|
value: settings.composerTranslationEnabled,
|
||||||
|
onChanged: settingsService.setComposerTranslationEnabled,
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.language),
|
||||||
|
title: Text(context.l10n.translation_targetLanguage),
|
||||||
|
subtitle: Text(
|
||||||
|
_translationLanguageLabel(
|
||||||
|
context,
|
||||||
|
settings.translationTargetLanguageCode,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () =>
|
||||||
|
_showTranslationLanguageDialog(context, settingsService),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||||
|
child: DropdownButtonFormField<String>(
|
||||||
|
initialValue: settings.translationSelectedModelId,
|
||||||
|
isExpanded: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: context.l10n.translation_downloadedModelLabel,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
for (final model in settings.translationDownloadedModels)
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: model.id,
|
||||||
|
child: Text(translationModelFriendlyName(model)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: settings.translationDownloadedModels.isEmpty
|
||||||
|
? null
|
||||||
|
: (value) {
|
||||||
|
settingsService.setTranslationSelectedModelId(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||||
|
child: DropdownButtonFormField<String>(
|
||||||
|
initialValue: null,
|
||||||
|
isExpanded: true,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: context.l10n.translation_presetModelLabel,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
for (final preset in translationPresetModels)
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: preset.sourceUrl,
|
||||||
|
child: Text(translationModelFriendlyName(preset)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: translationService.isBusy
|
||||||
|
? null
|
||||||
|
: (value) async {
|
||||||
|
if (value == null) return;
|
||||||
|
final preset = translationPresetModels.firstWhere(
|
||||||
|
(entry) => entry.sourceUrl == value,
|
||||||
|
);
|
||||||
|
await _downloadTranslationModel(
|
||||||
|
context,
|
||||||
|
translationService,
|
||||||
|
settingsService,
|
||||||
|
sourceUrl: preset.sourceUrl,
|
||||||
|
fileName: preset.name,
|
||||||
|
id: preset.id,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_TranslationUrlField(
|
||||||
|
initialValue: settings.translationModelSourceUrl ?? '',
|
||||||
|
onChanged: settingsService.setTranslationModelSourceUrl,
|
||||||
|
onDownload: translationService.isBusy
|
||||||
|
? null
|
||||||
|
: (url) => _downloadTranslationModel(
|
||||||
|
context,
|
||||||
|
translationService,
|
||||||
|
settingsService,
|
||||||
|
sourceUrl: url,
|
||||||
|
),
|
||||||
|
downloadLabel: translationService.isDownloading
|
||||||
|
? context.l10n.translation_downloading
|
||||||
|
: translationService.isBusy
|
||||||
|
? context.l10n.translation_working
|
||||||
|
: context.l10n.translation_downloadModel,
|
||||||
|
isDownloading: translationService.isDownloading,
|
||||||
|
onCancel: translationService.cancelDownload,
|
||||||
|
labelText: context.l10n.translation_manualUrlLabel,
|
||||||
|
stopLabel: context.l10n.translation_stop,
|
||||||
|
),
|
||||||
|
if (translationService.isDownloading) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
LinearProgressIndicator(
|
||||||
|
value:
|
||||||
|
translationService.downloadFileName ==
|
||||||
|
'Merging chunks...'
|
||||||
|
? null
|
||||||
|
: translationService.downloadProgress,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
_downloadProgressLabel(context, translationService),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (settings.translationDownloadedModels.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
context.l10n.translation_downloadedModels,
|
||||||
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
for (final model in settings.translationDownloadedModels)
|
||||||
|
Card.outlined(
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
leading: Icon(
|
||||||
|
model.id == settings.translationSelectedModelId
|
||||||
|
? Icons.check_circle
|
||||||
|
: Icons.memory_outlined,
|
||||||
|
),
|
||||||
|
title: Text(translationModelFriendlyName(model)),
|
||||||
|
subtitle: Text(_downloadedModelLabel(model)),
|
||||||
|
trailing: IconButton(
|
||||||
|
tooltip: context.l10n.translation_deleteModel,
|
||||||
|
onPressed: translationService.isBusy
|
||||||
|
? null
|
||||||
|
: () => _deleteTranslationModel(
|
||||||
|
context,
|
||||||
|
translationService,
|
||||||
|
model,
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
),
|
||||||
|
onTap: () => settingsService
|
||||||
|
.setTranslationSelectedModelId(model.id),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (translationService.lastError != null) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
translationService.lastError!,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Fixed rendering issues
|
// Fixed rendering issues
|
||||||
Widget _buildBatteryCard(
|
Widget _buildBatteryCard(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
@@ -551,6 +925,12 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
return context.l10n.appSettings_languageRu;
|
return context.l10n.appSettings_languageRu;
|
||||||
case 'uk':
|
case 'uk':
|
||||||
return context.l10n.appSettings_languageUk;
|
return context.l10n.appSettings_languageUk;
|
||||||
|
case 'hu':
|
||||||
|
return context.l10n.appSettings_languageHu;
|
||||||
|
case 'ja':
|
||||||
|
return context.l10n.appSettings_languageJa;
|
||||||
|
case 'ko':
|
||||||
|
return context.l10n.appSettings_languageKo;
|
||||||
default:
|
default:
|
||||||
return context.l10n.appSettings_languageSystem;
|
return context.l10n.appSettings_languageSystem;
|
||||||
}
|
}
|
||||||
@@ -638,6 +1018,18 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
title: Text(context.l10n.appSettings_languageUk),
|
title: Text(context.l10n.appSettings_languageUk),
|
||||||
value: 'uk',
|
value: 'uk',
|
||||||
),
|
),
|
||||||
|
RadioListTile<String?>(
|
||||||
|
title: Text(context.l10n.appSettings_languageHu),
|
||||||
|
value: 'hu',
|
||||||
|
),
|
||||||
|
RadioListTile<String?>(
|
||||||
|
title: Text(context.l10n.appSettings_languageJa),
|
||||||
|
value: 'ja',
|
||||||
|
),
|
||||||
|
RadioListTile<String?>(
|
||||||
|
title: Text(context.l10n.appSettings_languageKo),
|
||||||
|
value: 'ko',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -706,6 +1098,164 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showUnitsDialog(
|
||||||
|
BuildContext context,
|
||||||
|
AppSettingsService settingsService,
|
||||||
|
) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text(context.l10n.appSettings_unitsTitle),
|
||||||
|
content: RadioGroup<UnitSystem>(
|
||||||
|
groupValue: settingsService.settings.unitSystem,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
settingsService.setUnitSystem(value);
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
title: Text(context.l10n.appSettings_unitsMetric),
|
||||||
|
leading: const Radio<UnitSystem>(value: UnitSystem.metric),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(context.l10n.appSettings_unitsImperial),
|
||||||
|
leading: const Radio<UnitSystem>(value: UnitSystem.imperial),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(context.l10n.common_close),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showTranslationLanguageDialog(
|
||||||
|
BuildContext context,
|
||||||
|
AppSettingsService settingsService,
|
||||||
|
) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => _TranslationLanguageDialogContent(
|
||||||
|
currentLanguageCode:
|
||||||
|
settingsService.settings.translationTargetLanguageCode,
|
||||||
|
onLanguageSelected: (value) {
|
||||||
|
settingsService.setTranslationTargetLanguageCode(value);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _downloadTranslationModel(
|
||||||
|
BuildContext context,
|
||||||
|
TranslationService translationService,
|
||||||
|
AppSettingsService settingsService, {
|
||||||
|
required String sourceUrl,
|
||||||
|
String? fileName,
|
||||||
|
String? id,
|
||||||
|
}) async {
|
||||||
|
if (sourceUrl.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(context.l10n.translation_enterUrlFirst)),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await translationService.downloadModel(
|
||||||
|
sourceUrl: sourceUrl,
|
||||||
|
fileName: fileName,
|
||||||
|
id: id,
|
||||||
|
);
|
||||||
|
if (!context.mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(context.l10n.translation_modelDownloaded)),
|
||||||
|
);
|
||||||
|
await settingsService.setTranslationEnabled(true);
|
||||||
|
} on TranslationDownloadCancelled {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(context.l10n.translation_downloadStopped)),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
context.l10n.translation_downloadFailed(error.toString()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _translationLanguageLabel(BuildContext context, String? languageCode) {
|
||||||
|
if (languageCode == null || languageCode.isEmpty) {
|
||||||
|
return context.l10n.translation_useAppLanguage;
|
||||||
|
}
|
||||||
|
for (final option in supportedTranslationLanguages) {
|
||||||
|
if (option.code == languageCode) {
|
||||||
|
return option.label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return languageCode.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _downloadProgressLabel(
|
||||||
|
BuildContext context,
|
||||||
|
TranslationService translationService,
|
||||||
|
) {
|
||||||
|
final fileName = translationService.downloadFileName ?? 'Model';
|
||||||
|
if (fileName == 'Merging chunks...') {
|
||||||
|
return context.l10n.translation_mergingChunks;
|
||||||
|
}
|
||||||
|
final currentMb = translationService.downloadedBytes / (1024 * 1024);
|
||||||
|
final totalBytes = translationService.downloadTotalBytes;
|
||||||
|
if (totalBytes == null || totalBytes <= 0) {
|
||||||
|
return '$fileName: ${currentMb.toStringAsFixed(1)} MB';
|
||||||
|
}
|
||||||
|
final totalMb = totalBytes / (1024 * 1024);
|
||||||
|
final percent = ((translationService.downloadProgress ?? 0) * 100)
|
||||||
|
.toStringAsFixed(0);
|
||||||
|
return '$fileName: ${currentMb.toStringAsFixed(1)} / ${totalMb.toStringAsFixed(1)} MB ($percent%)';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _deleteTranslationModel(
|
||||||
|
BuildContext context,
|
||||||
|
TranslationService translationService,
|
||||||
|
TranslationModelRecord model,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
await translationService.removeModel(model);
|
||||||
|
if (!context.mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
// TODO: l10n
|
||||||
|
content: Text('Deleted ${translationModelFriendlyName(model)}.'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Delete failed: $error')),
|
||||||
|
); // TODO: l10n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _downloadedModelLabel(TranslationModelRecord model) {
|
||||||
|
final sizeMb = model.fileSizeBytes / (1024 * 1024);
|
||||||
|
final source = model.sourceUrl.isEmpty ? model.name : model.sourceUrl;
|
||||||
|
return '${sizeMb.toStringAsFixed(1)} MB • $source';
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildDebugCard(
|
Widget _buildDebugCard(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
AppSettingsService settingsService,
|
AppSettingsService settingsService,
|
||||||
@@ -746,3 +1296,179 @@ class AppSettingsScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Owns the [TextEditingController] for the manual model URL field so it
|
||||||
|
/// survives rebuilds of the parent [Consumer3].
|
||||||
|
class _TranslationUrlField extends StatefulWidget {
|
||||||
|
const _TranslationUrlField({
|
||||||
|
required this.initialValue,
|
||||||
|
required this.onChanged,
|
||||||
|
required this.onDownload,
|
||||||
|
required this.downloadLabel,
|
||||||
|
required this.isDownloading,
|
||||||
|
required this.onCancel,
|
||||||
|
required this.labelText,
|
||||||
|
required this.stopLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String initialValue;
|
||||||
|
final ValueChanged<String> onChanged;
|
||||||
|
final void Function(String url)? onDownload;
|
||||||
|
final String downloadLabel;
|
||||||
|
final bool isDownloading;
|
||||||
|
final VoidCallback onCancel;
|
||||||
|
final String labelText;
|
||||||
|
final String stopLabel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_TranslationUrlField> createState() => _TranslationUrlFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TranslationUrlFieldState extends State<_TranslationUrlField> {
|
||||||
|
late final TextEditingController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = TextEditingController(text: widget.initialValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: widget.labelText,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
onChanged: widget.onChanged,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: FilledButton.icon(
|
||||||
|
onPressed: widget.onDownload == null
|
||||||
|
? null
|
||||||
|
: () => widget.onDownload!(_controller.text.trim()),
|
||||||
|
icon: const Icon(Icons.download),
|
||||||
|
label: Text(widget.downloadLabel),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.isDownloading) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: widget.onCancel,
|
||||||
|
icon: const Icon(Icons.stop_circle_outlined),
|
||||||
|
label: Text(widget.stopLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dialog content for choosing the translation target language.
|
||||||
|
/// Owns the search [TextEditingController] so it is properly disposed.
|
||||||
|
class _TranslationLanguageDialogContent extends StatefulWidget {
|
||||||
|
const _TranslationLanguageDialogContent({
|
||||||
|
required this.currentLanguageCode,
|
||||||
|
required this.onLanguageSelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? currentLanguageCode;
|
||||||
|
final ValueChanged<String?> onLanguageSelected;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_TranslationLanguageDialogContent> createState() =>
|
||||||
|
_TranslationLanguageDialogContentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TranslationLanguageDialogContentState
|
||||||
|
extends State<_TranslationLanguageDialogContent> {
|
||||||
|
late final TextEditingController _searchController;
|
||||||
|
List<TranslationLanguageOption> _filtered = supportedTranslationLanguages;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_searchController = TextEditingController();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_searchController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(context.l10n.translation_targetLanguage),
|
||||||
|
content: SizedBox(
|
||||||
|
width: 360,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _searchController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
prefixIcon: Icon(Icons.search),
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
final normalized = value.trim().toLowerCase();
|
||||||
|
setState(() {
|
||||||
|
_filtered = supportedTranslationLanguages.where((option) {
|
||||||
|
return option.label.toLowerCase().contains(normalized) ||
|
||||||
|
option.code.toLowerCase().contains(normalized);
|
||||||
|
}).toList();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Flexible(
|
||||||
|
child: RadioGroup<String?>(
|
||||||
|
groupValue: widget.currentLanguageCode,
|
||||||
|
onChanged: (value) {
|
||||||
|
widget.onLanguageSelected(value);
|
||||||
|
},
|
||||||
|
child: ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
children: [
|
||||||
|
RadioListTile<String?>(
|
||||||
|
value: null,
|
||||||
|
title: Text(context.l10n.translation_useAppLanguage),
|
||||||
|
),
|
||||||
|
for (final option in _filtered)
|
||||||
|
RadioListTile<String?>(
|
||||||
|
value: option.code,
|
||||||
|
title: Text(option.label),
|
||||||
|
subtitle: Text(option.code.toUpperCase()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(context.l10n.common_close),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
|
|||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../services/ble_debug_log_service.dart';
|
import '../services/ble_debug_log_service.dart';
|
||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
|
import '../widgets/adaptive_app_bar_title.dart';
|
||||||
|
|
||||||
enum _BleLogView { frames, rawLogRx }
|
enum _BleLogView { frames, rawLogRx }
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
|||||||
: rawEntries.isNotEmpty;
|
: rawEntries.isNotEmpty;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(context.l10n.debugLog_bleTitle),
|
title: AdaptiveAppBarTitle(context.l10n.debugLog_bleTitle),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: context.l10n.debugLog_copyLog,
|
tooltip: context.l10n.debugLog_copyLog,
|
||||||
@@ -117,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(''),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,66 +283,66 @@ 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;
|
final role = _deviceRoleLabel(flags & 0x0F);
|
||||||
offset += 64; // signature
|
final hasLocation = (flags & 0x10) != 0;
|
||||||
final flags = payload[offset++];
|
final hasFeature1 = (flags & 0x20) != 0;
|
||||||
final role = _deviceRoleLabel(flags & 0x0F);
|
final hasFeature2 = (flags & 0x40) != 0;
|
||||||
final hasLocation = (flags & 0x10) != 0;
|
final hasName = (flags & 0x80) != 0;
|
||||||
final hasFeature1 = (flags & 0x20) != 0;
|
String? name;
|
||||||
final hasFeature2 = (flags & 0x40) != 0;
|
double? lat;
|
||||||
final hasName = (flags & 0x80) != 0;
|
double? lon;
|
||||||
String? name;
|
if (hasLocation) {
|
||||||
double? lat;
|
lat = reader.readInt32LE() / 1000000.0;
|
||||||
double? lon;
|
lon = reader.readInt32LE() / 1000000.0;
|
||||||
if (hasLocation && payload.length >= offset + 8) {
|
}
|
||||||
lat = readInt32LE(payload, offset) / 1000000.0;
|
if (hasFeature1) reader.skipBytes(2);
|
||||||
lon = readInt32LE(payload, offset + 4) / 1000000.0;
|
if (hasFeature2) reader.skipBytes(2);
|
||||||
offset += 8;
|
if (hasName) {
|
||||||
|
name = reader.readCStringGreedy(maxNameSize);
|
||||||
|
}
|
||||||
|
final namePart = (name != null && name.isNotEmpty) ? ' name="$name"' : '';
|
||||||
|
final locPart = (lat != null && lon != null)
|
||||||
|
? ' loc=${lat.toStringAsFixed(6)},${lon.toStringAsFixed(6)}'
|
||||||
|
: '';
|
||||||
|
return 'ADVERT role=$role ts=$timestamp$namePart$locPart key=${pubKey.substring(0, 12)}…';
|
||||||
|
} catch (e) {
|
||||||
|
return 'ADVERT (invalid)';
|
||||||
}
|
}
|
||||||
if (hasFeature1) offset += 2;
|
|
||||||
if (hasFeature2) offset += 2;
|
|
||||||
if (hasName && payload.length > offset) {
|
|
||||||
final rawName = String.fromCharCodes(payload.sublist(offset));
|
|
||||||
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 locPart = (lat != null && lon != null)
|
|
||||||
? ' loc=${lat.toStringAsFixed(6)},${lon.toStringAsFixed(6)}'
|
|
||||||
: '';
|
|
||||||
return 'ADVERT role=$role ts=$timestamp$namePart$locPart key=${pubKey.substring(0, 12)}…';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String _decodeControlSummary(Uint8List payload) {
|
String _decodeControlSummary(Uint8List payload) {
|
||||||
if (payload.isEmpty) return 'CONTROL (empty)';
|
final reader = BufferReader(payload);
|
||||||
final flags = payload[0];
|
try {
|
||||||
final subType = flags & 0xF0;
|
final flags = reader.readByte();
|
||||||
if (subType == 0x80) {
|
final subType = flags & 0xF0;
|
||||||
if (payload.length < 6) return 'CONTROL DISCOVER_REQ (short)';
|
if (subType == 0x80) {
|
||||||
final typeFilter = payload[1];
|
if (payload.length < 6) return 'CONTROL DISCOVER_REQ (short)';
|
||||||
final tag = readUint32LE(payload, 2);
|
final typeFilter = reader.readByte();
|
||||||
final since = payload.length >= 10 ? readUint32LE(payload, 6) : 0;
|
final tag = reader.readInt32LE();
|
||||||
return 'CONTROL DISCOVER_REQ filter=0x${typeFilter.toRadixString(16).padLeft(2, '0')} tag=$tag since=$since';
|
final since = payload.length >= 10 ? reader.readInt32LE() : 0;
|
||||||
|
return 'CONTROL DISCOVER_REQ filter=0x${typeFilter.toRadixString(16).padLeft(2, '0')} tag=$tag since=$since';
|
||||||
|
}
|
||||||
|
if (subType == 0x90) {
|
||||||
|
if (payload.length < 14) return 'CONTROL DISCOVER_RESP (short)';
|
||||||
|
final nodeType = flags & 0x0F;
|
||||||
|
final snrRaw = payload[1];
|
||||||
|
final snrSigned = snrRaw > 127 ? snrRaw - 256 : snrRaw;
|
||||||
|
final snr = snrSigned / 4.0;
|
||||||
|
final tag = reader.readInt32LE();
|
||||||
|
final keyLen = payload.length - 6;
|
||||||
|
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')}';
|
||||||
|
} catch (e) {
|
||||||
|
return 'CONTROL (invalid)';
|
||||||
}
|
}
|
||||||
if (subType == 0x90) {
|
|
||||||
if (payload.length < 14) return 'CONTROL DISCOVER_RESP (short)';
|
|
||||||
final nodeType = flags & 0x0F;
|
|
||||||
final snrRaw = payload[1];
|
|
||||||
final snrSigned = snrRaw > 127 ? snrRaw - 256 : snrRaw;
|
|
||||||
final snr = snrSigned / 4.0;
|
|
||||||
final tag = readUint32LE(payload, 2);
|
|
||||||
final keyLen = payload.length - 6;
|
|
||||||
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')}';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String _payloadTypeLabel(int payloadType) {
|
String _payloadTypeLabel(int payloadType) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,27 +9,38 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
import '../services/map_tile_cache_service.dart';
|
import '../services/map_tile_cache_service.dart';
|
||||||
|
import '../services/app_settings_service.dart';
|
||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../models/channel_message.dart';
|
import '../models/channel_message.dart';
|
||||||
|
import '../models/app_settings.dart';
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
|
import '../widgets/adaptive_app_bar_title.dart';
|
||||||
|
|
||||||
class ChannelMessagePathScreen extends StatelessWidget {
|
class ChannelMessagePathScreen extends StatelessWidget {
|
||||||
final ChannelMessage message;
|
final ChannelMessage message;
|
||||||
|
final bool channelMessage;
|
||||||
const ChannelMessagePathScreen({super.key, required this.message});
|
const ChannelMessagePathScreen({
|
||||||
|
super.key,
|
||||||
|
required this.message,
|
||||||
|
this.channelMessage = false,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<MeshCoreConnector>(
|
return Consumer<MeshCoreConnector>(
|
||||||
builder: (context, connector, _) {
|
builder: (context, connector, _) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
final primaryPath = _selectPrimaryPath(
|
final primaryPathTmp = _selectPrimaryPath(
|
||||||
message.pathBytes,
|
message.pathBytes,
|
||||||
message.pathVariants,
|
message.pathVariants,
|
||||||
);
|
);
|
||||||
final hops = _buildPathHops(primaryPath, connector.contacts, l10n);
|
|
||||||
|
final primaryPath = !channelMessage && !message.isOutgoing
|
||||||
|
? Uint8List.fromList(primaryPathTmp.reversed.toList())
|
||||||
|
: primaryPathTmp;
|
||||||
|
final hops = _buildPathHops(primaryPath, connector, l10n);
|
||||||
final hasHopDetails = primaryPath.isNotEmpty;
|
final hasHopDetails = primaryPath.isNotEmpty;
|
||||||
final observedLabel = _formatObservedHops(
|
final observedLabel = _formatObservedHops(
|
||||||
primaryPath.length,
|
primaryPath.length,
|
||||||
@@ -37,10 +48,9 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
l10n,
|
l10n,
|
||||||
);
|
);
|
||||||
final extraPaths = _otherPaths(primaryPath, message.pathVariants);
|
final extraPaths = _otherPaths(primaryPath, message.pathVariants);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(l10n.channelPath_title),
|
title: AdaptiveAppBarTitle(l10n.channelPath_title),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.radar_outlined),
|
icon: const Icon(Icons.radar_outlined),
|
||||||
@@ -50,9 +60,13 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => PathTraceMapScreen(
|
builder: (context) => PathTraceMapScreen(
|
||||||
title: context.l10n.contacts_repeaterPathTrace,
|
title: context.l10n.contacts_repeaterPathTrace,
|
||||||
path: Uint8List.fromList(primaryPath),
|
path: primaryPath,
|
||||||
flipPathRound: true,
|
flipPathAround: true,
|
||||||
reversePathRound: true,
|
reversePathAround:
|
||||||
|
!(!channelMessage && !message.isOutgoing),
|
||||||
|
pathHashByteWidth: context
|
||||||
|
.read<MeshCoreConnector>()
|
||||||
|
.pathHashByteWidth,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -62,7 +76,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
tooltip: l10n.channelPath_viewMap,
|
tooltip: l10n.channelPath_viewMap,
|
||||||
onPressed: hasHopDetails
|
onPressed: hasHopDetails
|
||||||
? () {
|
? () {
|
||||||
_openPathMap(context);
|
_openPathMap(context, channelMessage: channelMessage);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@@ -157,7 +171,11 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
subtitle: Text(_formatPathPrefixes(variants[i])),
|
subtitle: Text(_formatPathPrefixes(variants[i])),
|
||||||
trailing: const Icon(Icons.map_outlined, size: 20),
|
trailing: const Icon(Icons.map_outlined, size: 20),
|
||||||
onTap: () => _openPathMap(context, initialPath: variants[i]),
|
onTap: () => _openPathMap(
|
||||||
|
context,
|
||||||
|
initialPath: variants[i],
|
||||||
|
channelMessage: channelMessage,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -248,13 +266,18 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _openPathMap(BuildContext context, {Uint8List? initialPath}) {
|
void _openPathMap(
|
||||||
|
BuildContext context, {
|
||||||
|
Uint8List? initialPath,
|
||||||
|
bool channelMessage = false,
|
||||||
|
}) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => ChannelMessagePathMapScreen(
|
builder: (context) => ChannelMessagePathMapScreen(
|
||||||
message: message,
|
message: message,
|
||||||
initialPath: initialPath,
|
initialPath: initialPath,
|
||||||
|
channelMessage: channelMessage,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -264,11 +287,13 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
class ChannelMessagePathMapScreen extends StatefulWidget {
|
class ChannelMessagePathMapScreen extends StatefulWidget {
|
||||||
final ChannelMessage message;
|
final ChannelMessage message;
|
||||||
final Uint8List? initialPath;
|
final Uint8List? initialPath;
|
||||||
|
final bool channelMessage;
|
||||||
|
|
||||||
const ChannelMessagePathMapScreen({
|
const ChannelMessagePathMapScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.message,
|
required this.message,
|
||||||
this.initialPath,
|
this.initialPath,
|
||||||
|
this.channelMessage = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -278,8 +303,14 @@ class ChannelMessagePathMapScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _ChannelMessagePathMapScreenState
|
class _ChannelMessagePathMapScreenState
|
||||||
extends State<ChannelMessagePathMapScreen> {
|
extends State<ChannelMessagePathMapScreen> {
|
||||||
|
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 _didReceivePositionUpdate = false;
|
||||||
|
int? _focusedHopIndex;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -310,10 +341,28 @@ 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>(
|
||||||
builder: (context, connector, _) {
|
builder: (context, connector, _) {
|
||||||
|
final settings = context.watch<AppSettingsService>().settings;
|
||||||
|
final isImperial = settings.unitSystem == UnitSystem.imperial;
|
||||||
final tileCache = context.read<MapTileCacheService>();
|
final tileCache = context.read<MapTileCacheService>();
|
||||||
final primaryPath = _selectPrimaryPath(
|
final primaryPath = _selectPrimaryPath(
|
||||||
widget.message.pathBytes,
|
widget.message.pathBytes,
|
||||||
@@ -323,25 +372,38 @@ class _ChannelMessagePathMapScreenState
|
|||||||
primaryPath,
|
primaryPath,
|
||||||
widget.message.pathVariants,
|
widget.message.pathVariants,
|
||||||
);
|
);
|
||||||
final selectedPath = _resolveSelectedPath(
|
final selectedPathTmp = _resolveSelectedPath(
|
||||||
_selectedPath,
|
_selectedPath,
|
||||||
observedPaths,
|
observedPaths,
|
||||||
primaryPath,
|
primaryPath,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final selectedPath =
|
||||||
|
((!widget.message.isOutgoing && !widget.channelMessage) ||
|
||||||
|
(widget.message.isOutgoing && widget.channelMessage))
|
||||||
|
? Uint8List.fromList(selectedPathTmp.reversed.toList())
|
||||||
|
: selectedPathTmp;
|
||||||
|
|
||||||
final selectedIndex = _indexForPath(selectedPath, observedPaths);
|
final selectedIndex = _indexForPath(selectedPath, observedPaths);
|
||||||
final hops = _buildPathHops(
|
final hops = _buildPathHops(selectedPath, connector, context.l10n);
|
||||||
selectedPath,
|
|
||||||
connector.contacts,
|
|
||||||
context.l10n,
|
|
||||||
);
|
|
||||||
|
|
||||||
final points = <LatLng>[];
|
final points = <LatLng>[];
|
||||||
|
|
||||||
|
if ((widget.message.isOutgoing && !widget.channelMessage) ||
|
||||||
|
(widget.message.isOutgoing && widget.channelMessage)) {
|
||||||
|
points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
||||||
|
}
|
||||||
|
|
||||||
for (final hop in hops) {
|
for (final hop in hops) {
|
||||||
if (hop.hasLocation) {
|
if (hop.hasLocation) {
|
||||||
points.add(hop.position!);
|
points.add(hop.position!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
|
||||||
|
if ((!widget.message.isOutgoing && !widget.channelMessage) ||
|
||||||
|
(!widget.message.isOutgoing && widget.channelMessage)) {
|
||||||
|
points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
||||||
|
}
|
||||||
|
|
||||||
final polylines = points.length > 1
|
final polylines = points.length > 1
|
||||||
? [
|
? [
|
||||||
@@ -357,6 +419,9 @@ class _ChannelMessagePathMapScreenState
|
|||||||
? points.first
|
? points.first
|
||||||
: const LatLng(0, 0);
|
: const LatLng(0, 0);
|
||||||
final initialZoom = points.isNotEmpty ? 13.0 : 2.0;
|
final initialZoom = points.isNotEmpty ? 13.0 : 2.0;
|
||||||
|
if (!_didReceivePositionUpdate) {
|
||||||
|
_showNodeLabels = initialZoom >= _labelZoomThreshold;
|
||||||
|
}
|
||||||
final bounds = points.length > 1
|
final bounds = points.length > 1
|
||||||
? LatLngBounds.fromPoints(points)
|
? LatLngBounds.fromPoints(points)
|
||||||
: null;
|
: null;
|
||||||
@@ -366,13 +431,16 @@ class _ChannelMessagePathMapScreenState
|
|||||||
_pathDistance = _getPathDistance(points);
|
_pathDistance = _getPathDistance(points);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: Text(context.l10n.channelPath_mapTitle)),
|
appBar: AppBar(
|
||||||
|
title: AdaptiveAppBarTitle(context.l10n.channelPath_mapTitle),
|
||||||
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
FlutterMap(
|
FlutterMap(
|
||||||
key: mapKey,
|
key: mapKey,
|
||||||
|
mapController: _mapController,
|
||||||
options: MapOptions(
|
options: MapOptions(
|
||||||
initialCenter: initialCenter,
|
initialCenter: initialCenter,
|
||||||
initialZoom: initialZoom,
|
initialZoom: initialZoom,
|
||||||
@@ -388,6 +456,17 @@ class _ChannelMessagePathMapScreenState
|
|||||||
interactionOptions: InteractionOptions(
|
interactionOptions: InteractionOptions(
|
||||||
flags: ~InteractiveFlag.rotate,
|
flags: ~InteractiveFlag.rotate,
|
||||||
),
|
),
|
||||||
|
onPositionChanged: (camera, hasGesture) {
|
||||||
|
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
||||||
|
if (!_didReceivePositionUpdate ||
|
||||||
|
shouldShow != _showNodeLabels) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_didReceivePositionUpdate = true;
|
||||||
|
_showNodeLabels = shouldShow;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
TileLayer(
|
TileLayer(
|
||||||
@@ -399,7 +478,12 @@ class _ChannelMessagePathMapScreenState
|
|||||||
),
|
),
|
||||||
if (polylines.isNotEmpty)
|
if (polylines.isNotEmpty)
|
||||||
PolylineLayer(polylines: polylines),
|
PolylineLayer(polylines: polylines),
|
||||||
MarkerLayer(markers: _buildHopMarkers(hops)),
|
MarkerLayer(
|
||||||
|
markers: _buildHopMarkers(
|
||||||
|
hops,
|
||||||
|
showLabels: _showNodeLabels,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (observedPaths.length > 1)
|
if (observedPaths.length > 1)
|
||||||
@@ -408,6 +492,7 @@ class _ChannelMessagePathMapScreenState
|
|||||||
) {
|
) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedPath = observedPaths[index].pathBytes;
|
_selectedPath = observedPaths[index].pathBytes;
|
||||||
|
_focusedHopIndex = null;
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
if (points.isEmpty)
|
if (points.isEmpty)
|
||||||
@@ -422,7 +507,7 @@ class _ChannelMessagePathMapScreenState
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildLegendCard(context, hops),
|
_buildLegendCard(context, hops, isImperial),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -494,45 +579,61 @@ class _ChannelMessagePathMapScreenState
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Marker> _buildHopMarkers(List<_PathHop> hops) {
|
List<Marker> _buildHopMarkers(
|
||||||
return [
|
List<_PathHop> hops, {
|
||||||
for (final hop in hops)
|
required bool showLabels,
|
||||||
if (hop.hasLocation)
|
}) {
|
||||||
Marker(
|
final markers = <Marker>[];
|
||||||
point: hop.position!,
|
for (final hop in hops) {
|
||||||
width: 35,
|
if (!hop.hasLocation) continue;
|
||||||
height: 35,
|
final point = hop.position!;
|
||||||
child: Container(
|
markers.add(
|
||||||
decoration: BoxDecoration(
|
Marker(
|
||||||
color: Colors.green,
|
point: point,
|
||||||
shape: BoxShape.circle,
|
width: 35,
|
||||||
border: Border.all(color: Colors.white, width: 2),
|
height: 35,
|
||||||
boxShadow: [
|
child: Container(
|
||||||
BoxShadow(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black.withValues(alpha: 0.3),
|
color: Colors.green,
|
||||||
blurRadius: 4,
|
shape: BoxShape.circle,
|
||||||
offset: const Offset(0, 2),
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
),
|
boxShadow: [
|
||||||
],
|
BoxShadow(
|
||||||
),
|
color: Colors.black.withValues(alpha: 0.3),
|
||||||
alignment: Alignment.center,
|
blurRadius: 4,
|
||||||
child: Text(
|
offset: const Offset(0, 2),
|
||||||
hop.index.toString(),
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
hop.index.toString(),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (context.read<MeshCoreConnector>().selfLatitude != null &&
|
),
|
||||||
context.read<MeshCoreConnector>().selfLongitude != null)
|
);
|
||||||
Marker(
|
if (showLabels) {
|
||||||
point: LatLng(
|
markers.add(
|
||||||
context.read<MeshCoreConnector>().selfLatitude!,
|
_buildNodeLabelMarker(
|
||||||
context.read<MeshCoreConnector>().selfLongitude!,
|
point: point,
|
||||||
|
label: hop.contact?.name ?? _formatPrefix(hop.prefix),
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final selfLat = context.read<MeshCoreConnector>().selfLatitude;
|
||||||
|
final selfLon = context.read<MeshCoreConnector>().selfLongitude;
|
||||||
|
if (selfLat != null && selfLon != null) {
|
||||||
|
final selfPoint = LatLng(selfLat, selfLon);
|
||||||
|
markers.add(
|
||||||
|
Marker(
|
||||||
|
point: selfPoint,
|
||||||
width: 35,
|
width: 35,
|
||||||
height: 35,
|
height: 35,
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -559,10 +660,60 @@ class _ChannelMessagePathMapScreenState
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
);
|
||||||
|
if (showLabels) {
|
||||||
|
markers.add(
|
||||||
|
_buildNodeLabelMarker(
|
||||||
|
point: selfPoint,
|
||||||
|
label: context.l10n.pathTrace_you,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return markers;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLegendCard(BuildContext context, List<_PathHop> hops) {
|
Marker _buildNodeLabelMarker({required LatLng point, required String label}) {
|
||||||
|
return Marker(
|
||||||
|
point: point,
|
||||||
|
width: 120,
|
||||||
|
height: 24,
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: Transform.translate(
|
||||||
|
offset: const Offset(0, -20),
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black54,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLegendCard(
|
||||||
|
BuildContext context,
|
||||||
|
List<_PathHop> hops,
|
||||||
|
bool isImperial,
|
||||||
|
) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
final maxHeight = MediaQuery.of(context).size.height * 0.35;
|
final maxHeight = MediaQuery.of(context).size.height * 0.35;
|
||||||
final estimatedHeight = 72.0 + (hops.length * 56.0);
|
final estimatedHeight = 72.0 + (hops.length * 56.0);
|
||||||
@@ -581,7 +732,7 @@ class _ChannelMessagePathMapScreenState
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
child: Text(
|
child: Text(
|
||||||
'${l10n.channelPath_repeaterHops} (${(_pathDistance / 1609.34).toStringAsFixed(2)} Miles / ${(_pathDistance / 1000).toStringAsFixed(2)} Km)',
|
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistance, isImperial: isImperial)}',
|
||||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -597,8 +748,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(
|
||||||
@@ -657,19 +817,83 @@ 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>>{};
|
||||||
|
final allContacts = connector.allContacts;
|
||||||
|
for (final contact in 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();
|
||||||
|
var lastDistance = 0.0;
|
||||||
|
var bestDistance = 0.0;
|
||||||
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) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
// If the best candidate is much farther than the previous hop, it's likely not the correct match.
|
||||||
|
if (lastDistance + bestDistance > 50000 &&
|
||||||
|
candidates != null &&
|
||||||
|
candidates.isNotEmpty) {
|
||||||
|
i--;
|
||||||
|
lastDistance = bestDistance;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
lastDistance = bestDistance;
|
||||||
|
|
||||||
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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -677,42 +901,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) {
|
||||||
|
|||||||
+258
-153
@@ -3,18 +3,22 @@ import 'dart:math';
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:meshcore_open/storage/channel_message_store.dart';
|
||||||
|
import 'package:meshcore_open/utils/platform_info.dart';
|
||||||
|
import 'package:meshcore_open/widgets/app_bar.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
import '../connector/meshcore_connector.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
|
import '../services/app_settings_service.dart';
|
||||||
|
import '../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';
|
||||||
import '../utils/dialog_utils.dart';
|
import '../utils/dialog_utils.dart';
|
||||||
import '../utils/disconnect_navigation_mixin.dart';
|
import '../utils/disconnect_navigation_mixin.dart';
|
||||||
import '../utils/route_transitions.dart';
|
import '../utils/route_transitions.dart';
|
||||||
import '../widgets/battery_indicator.dart';
|
|
||||||
import '../widgets/list_filter_widget.dart';
|
import '../widgets/list_filter_widget.dart';
|
||||||
import '../widgets/empty_state.dart';
|
import '../widgets/empty_state.dart';
|
||||||
import '../widgets/qr_code_display.dart';
|
import '../widgets/qr_code_display.dart';
|
||||||
@@ -26,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;
|
||||||
|
|
||||||
@@ -41,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();
|
||||||
@@ -59,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(() {
|
||||||
@@ -104,6 +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();
|
||||||
|
channelMessageStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||||
|
|
||||||
// Auto-navigate back to scanner if disconnected
|
// Auto-navigate back to scanner if disconnected
|
||||||
if (!checkConnectionAndNavigate(connector)) {
|
if (!checkConnectionAndNavigate(connector)) {
|
||||||
@@ -116,8 +127,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
canPop: allowBack,
|
canPop: allowBack,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: BatteryIndicator(connector: connector),
|
title: AppBarTitle(context.l10n.channels_title),
|
||||||
title: Text(context.l10n.channels_title),
|
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
actions: [
|
actions: [
|
||||||
@@ -197,6 +207,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
final filteredChannels = _filterAndSortChannels(
|
final filteredChannels = _filterAndSortChannels(
|
||||||
channels,
|
channels,
|
||||||
connector,
|
connector,
|
||||||
|
viewState,
|
||||||
);
|
);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
@@ -211,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(
|
||||||
@@ -238,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);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -275,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,
|
||||||
@@ -304,6 +318,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
return _buildChannelTile(
|
return _buildChannelTile(
|
||||||
context,
|
context,
|
||||||
connector,
|
connector,
|
||||||
|
channelMessageStore,
|
||||||
channel,
|
channel,
|
||||||
showDragHandle: true,
|
showDragHandle: true,
|
||||||
dragIndex: index,
|
dragIndex: index,
|
||||||
@@ -323,6 +338,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
return _buildChannelTile(
|
return _buildChannelTile(
|
||||||
context,
|
context,
|
||||||
connector,
|
connector,
|
||||||
|
channelMessageStore,
|
||||||
channel,
|
channel,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -352,6 +368,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
Widget _buildChannelTile(
|
Widget _buildChannelTile(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
MeshCoreConnector connector,
|
MeshCoreConnector connector,
|
||||||
|
ChannelMessageStore channelMessageStore,
|
||||||
Channel channel, {
|
Channel channel, {
|
||||||
bool showDragHandle = false,
|
bool showDragHandle = false,
|
||||||
int? dragIndex,
|
int? dragIndex,
|
||||||
@@ -401,74 +418,97 @@ 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: ListTile(
|
child: GestureDetector(
|
||||||
dense: true,
|
onSecondaryTapUp: PlatformInfo.isDesktop
|
||||||
minVerticalPadding: 0,
|
? (_) => _showChannelActions(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
context,
|
||||||
visualDensity: const VisualDensity(vertical: -2),
|
connector,
|
||||||
leading: Stack(
|
channelMessageStore,
|
||||||
children: [
|
channel,
|
||||||
CircleAvatar(
|
)
|
||||||
backgroundColor: bgColor,
|
: null,
|
||||||
child: Icon(icon, color: iconColor),
|
child: ListTile(
|
||||||
),
|
dense: true,
|
||||||
if (isCommunityChannel)
|
minVerticalPadding: 0,
|
||||||
Positioned(
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
right: 0,
|
visualDensity: const VisualDensity(vertical: -2),
|
||||||
bottom: 0,
|
leading: Stack(
|
||||||
child: Container(
|
children: [
|
||||||
width: 14,
|
CircleAvatar(
|
||||||
height: 14,
|
backgroundColor: bgColor,
|
||||||
decoration: BoxDecoration(
|
child: Icon(icon, color: iconColor),
|
||||||
color: Colors.purple,
|
),
|
||||||
shape: BoxShape.circle,
|
if (isCommunityChannel)
|
||||||
border: Border.all(
|
Positioned(
|
||||||
color: Theme.of(context).cardColor,
|
right: 0,
|
||||||
width: 2,
|
bottom: 0,
|
||||||
|
child: Container(
|
||||||
|
width: 14,
|
||||||
|
height: 14,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.purple,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).cardColor,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.people,
|
||||||
|
size: 8,
|
||||||
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.people, size: 8, color: Colors.white),
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
channel.name.isEmpty
|
|
||||||
? context.l10n.channels_channelIndex(channel.index)
|
|
||||||
: channel.name,
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
|
||||||
),
|
|
||||||
subtitle: Text(subtitle, maxLines: 1, overflow: TextOverflow.ellipsis),
|
|
||||||
trailing: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
if (unreadCount > 0) ...[
|
|
||||||
UnreadBadge(count: unreadCount),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
],
|
],
|
||||||
if (showDragHandle && dragIndex != null)
|
),
|
||||||
ReorderableDelayedDragStartListener(
|
title: Text(
|
||||||
index: dragIndex,
|
channel.name.isEmpty
|
||||||
child: Icon(
|
? context.l10n.channels_channelIndex(channel.index)
|
||||||
Icons.drag_handle,
|
: channel.name,
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
subtitle,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (unreadCount > 0) ...[
|
||||||
|
UnreadBadge(count: unreadCount),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
],
|
||||||
|
if (showDragHandle && dragIndex != null)
|
||||||
|
ReorderableDelayedDragStartListener(
|
||||||
|
index: dragIndex,
|
||||||
|
child: Icon(
|
||||||
|
Icons.drag_handle,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
|
onTap: () async {
|
||||||
|
connector.markChannelRead(channel.index);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 50));
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => ChannelChatScreen(channel: channel),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongPress: () => _showChannelActions(
|
||||||
|
context,
|
||||||
|
connector,
|
||||||
|
channelMessageStore,
|
||||||
|
channel,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: () async {
|
|
||||||
connector.markChannelRead(channel.index);
|
|
||||||
await Future.delayed(const Duration(milliseconds: 50));
|
|
||||||
if (context.mounted) {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => ChannelChatScreen(channel: channel),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongPress: () => _showChannelActions(context, connector, channel),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -476,11 +516,16 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
void _showChannelActions(
|
void _showChannelActions(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
MeshCoreConnector connector,
|
MeshCoreConnector connector,
|
||||||
|
ChannelMessageStore channelMessageStore,
|
||||||
Channel channel,
|
Channel channel,
|
||||||
) {
|
) {
|
||||||
|
final parentContext = context;
|
||||||
|
final settingsService = context.read<AppSettingsService>();
|
||||||
|
final isMuted = settingsService.isChannelMuted(channel.name);
|
||||||
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: parentContext,
|
||||||
builder: (context) => SafeArea(
|
builder: (sheetContext) => SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -488,10 +533,30 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
leading: const Icon(Icons.edit_outlined),
|
leading: const Icon(Icons.edit_outlined),
|
||||||
title: Text(context.l10n.channels_editChannel),
|
title: Text(context.l10n.channels_editChannel),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
Navigator.pop(context);
|
Navigator.pop(sheetContext);
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
if (context.mounted) {
|
if (parentContext.mounted) {
|
||||||
_showEditChannelDialog(context, connector, channel);
|
_showEditChannelDialog(parentContext, connector, channel);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
isMuted
|
||||||
|
? Icons.notifications_outlined
|
||||||
|
: Icons.notifications_off_outlined,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
isMuted
|
||||||
|
? context.l10n.channels_unmuteChannel
|
||||||
|
: context.l10n.channels_muteChannel,
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
Navigator.pop(sheetContext);
|
||||||
|
if (isMuted) {
|
||||||
|
await settingsService.unmuteChannel(channel.name);
|
||||||
|
} else {
|
||||||
|
await settingsService.muteChannel(channel.name);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -502,10 +567,15 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
style: const TextStyle(color: Colors.red),
|
style: const TextStyle(color: Colors.red),
|
||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
Navigator.pop(context);
|
Navigator.pop(sheetContext);
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
if (context.mounted) {
|
if (parentContext.mounted) {
|
||||||
_confirmDeleteChannel(context, connector, channel);
|
_confirmDeleteChannel(
|
||||||
|
context,
|
||||||
|
connector,
|
||||||
|
channelMessageStore,
|
||||||
|
channel,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -538,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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -598,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) {
|
||||||
@@ -611,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:
|
||||||
@@ -672,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(
|
||||||
@@ -723,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(
|
||||||
@@ -748,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(
|
||||||
@@ -770,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(
|
||||||
@@ -1289,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,
|
||||||
@@ -1298,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(
|
||||||
@@ -1308,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(
|
||||||
@@ -1318,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,
|
||||||
@@ -1326,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,
|
||||||
@@ -1334,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)!,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1415,7 +1486,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
child: Text(dialogContext.l10n.common_cancel),
|
child: Text(dialogContext.l10n.common_cancel),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
final name = nameController.text.trim();
|
final name = nameController.text.trim();
|
||||||
final pskHex = pskController.text.trim();
|
final pskHex = pskController.text.trim();
|
||||||
|
|
||||||
@@ -1432,13 +1503,25 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Navigator.pop(dialogContext);
|
Navigator.pop(dialogContext);
|
||||||
connector.setChannel(channel.index, name, psk);
|
try {
|
||||||
connector.setChannelSmazEnabled(channel.index, smazEnabled);
|
await connector.setChannel(channel.index, name, psk);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
await connector.setChannelSmazEnabled(
|
||||||
SnackBar(
|
channel.index,
|
||||||
content: Text(context.l10n.channels_channelUpdated(name)),
|
smazEnabled,
|
||||||
),
|
);
|
||||||
);
|
if (!context.mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(context.l10n.channels_channelUpdated(name)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e, st) {
|
||||||
|
debugPrint(st.toString());
|
||||||
|
if (!context.mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Failed to update channel: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: Text(dialogContext.l10n.common_save),
|
child: Text(dialogContext.l10n.common_save),
|
||||||
),
|
),
|
||||||
@@ -1451,6 +1534,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
void _confirmDeleteChannel(
|
void _confirmDeleteChannel(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
MeshCoreConnector connector,
|
MeshCoreConnector connector,
|
||||||
|
ChannelMessageStore channelMessageStore,
|
||||||
Channel channel,
|
Channel channel,
|
||||||
) {
|
) {
|
||||||
showDialog(
|
showDialog(
|
||||||
@@ -1466,16 +1550,36 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||||||
child: Text(dialogContext.l10n.common_cancel),
|
child: Text(dialogContext.l10n.common_cancel),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
Navigator.pop(dialogContext);
|
Navigator.pop(dialogContext);
|
||||||
connector.deleteChannel(channel.index);
|
try {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
await connector.deleteChannel(channel.index);
|
||||||
SnackBar(
|
|
||||||
content: Text(
|
await channelMessageStore.clearChannelMessages(channel.index);
|
||||||
context.l10n.channels_channelDeleted(channel.name),
|
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
context.l10n.channels_channelDeleted(channel.name),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
} catch (e, st) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
context.l10n.channels_channelDeleteFailed(channel.name),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Preserve existing logging (if it was there)
|
||||||
|
debugPrint('Failed to delete channel: $e\n$st');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
dialogContext.l10n.common_delete,
|
dialogContext.l10n.common_delete,
|
||||||
@@ -1676,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,
|
||||||
|
|||||||
+1090
-489
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,89 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../l10n/l10n.dart';
|
||||||
|
|
||||||
|
class ChromeRequiredScreen extends StatelessWidget {
|
||||||
|
const ChromeRequiredScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final isDark = theme.brightness == Brightness.dark;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: isDark
|
||||||
|
? [const Color(0xFF1A1A1A), const Color(0xFF0D0D0D)]
|
||||||
|
: [const Color(0xFFF5F7FA), const Color(0xFFE4E7EB)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.orange.withValues(alpha: 0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.browser_not_supported_rounded,
|
||||||
|
size: 80,
|
||||||
|
color: Colors.orange,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
Text(
|
||||||
|
l10n.scanner_chromeRequired,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: theme.textTheme.headlineMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: isDark ? Colors.white : Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
l10n.scanner_chromeRequiredMessage,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: isDark ? Colors.white70 : Colors.black54,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 48),
|
||||||
|
// We can't really "fix" it for them other than telling them to use Chrome
|
||||||
|
// but we can provide a nice visual.
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue.withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(30),
|
||||||
|
border: Border.all(color: Colors.blue.withValues(alpha: 0.3)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.info_outline, size: 20, color: Colors.blue),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
"Web Bluetooth requires a Chromium browser",
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Colors.blue,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import '../connector/meshcore_connector.dart';
|
|||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../models/community.dart';
|
import '../models/community.dart';
|
||||||
import '../storage/community_store.dart';
|
import '../storage/community_store.dart';
|
||||||
|
import '../widgets/adaptive_app_bar_title.dart';
|
||||||
import '../widgets/qr_scanner_widget.dart';
|
import '../widgets/qr_scanner_widget.dart';
|
||||||
|
|
||||||
/// Screen for scanning community QR codes to join communities.
|
/// Screen for scanning community QR codes to join communities.
|
||||||
@@ -29,7 +30,7 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(context.l10n.community_scanQr),
|
title: AdaptiveAppBarTitle(context.l10n.community_scanQr),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
),
|
),
|
||||||
body: _isProcessing
|
body: _isProcessing
|
||||||
@@ -50,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);
|
||||||
@@ -208,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
|
||||||
|
|||||||
@@ -0,0 +1,252 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:meshcore_open/connector/meshcore_connector.dart';
|
||||||
|
import 'package:meshcore_open/models/companion_radio_stats.dart';
|
||||||
|
import 'package:meshcore_open/l10n/l10n.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class CompanionRadioStatsScreen extends StatefulWidget {
|
||||||
|
const CompanionRadioStatsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CompanionRadioStatsScreen> createState() =>
|
||||||
|
_CompanionRadioStatsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CompanionRadioStatsScreenState extends State<CompanionRadioStatsScreen> {
|
||||||
|
final List<double> _noiseHistory = [];
|
||||||
|
static const int _maxSamples = 120;
|
||||||
|
MeshCoreConnector? _connector;
|
||||||
|
DateTime? _lastChartSampleAt;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final c = context.read<MeshCoreConnector>();
|
||||||
|
_connector = c;
|
||||||
|
c.acquireRadioStatsPolling();
|
||||||
|
c.setPollingInterval(1);
|
||||||
|
c.radioStatsNotifier.addListener(_onStatsUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onStatsUpdate() {
|
||||||
|
final s = _connector?.radioStatsNotifier.value;
|
||||||
|
if (s == null || !mounted) return;
|
||||||
|
if (_lastChartSampleAt == s.receivedAt) return;
|
||||||
|
_lastChartSampleAt = s.receivedAt;
|
||||||
|
setState(() {
|
||||||
|
_noiseHistory.add(s.noiseFloorDbm.toDouble());
|
||||||
|
while (_noiseHistory.length > _maxSamples) {
|
||||||
|
_noiseHistory.removeAt(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_connector?.radioStatsNotifier.removeListener(_onStatsUpdate);
|
||||||
|
_connector?.releaseRadioStatsPolling();
|
||||||
|
_connector?.setPollingInterval(30);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(l10n.radioStats_screenTitle),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: Selector<MeshCoreConnector, ({bool connected, bool supported})>(
|
||||||
|
selector: (_, c) => (
|
||||||
|
connected: c.isConnected,
|
||||||
|
supported: c.supportsCompanionRadioStats,
|
||||||
|
),
|
||||||
|
builder: (context, state, _) {
|
||||||
|
if (!state.connected) {
|
||||||
|
return Center(child: Text(l10n.radioStats_notConnected));
|
||||||
|
}
|
||||||
|
if (!state.supported) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Text(
|
||||||
|
l10n.radioStats_firmwareTooOld,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final connector = context.read<MeshCoreConnector>();
|
||||||
|
final scheme = Theme.of(context).colorScheme;
|
||||||
|
final tt = Theme.of(context).textTheme;
|
||||||
|
|
||||||
|
return ValueListenableBuilder<CompanionRadioStats?>(
|
||||||
|
valueListenable: connector.radioStatsNotifier,
|
||||||
|
builder: (context, stats, _) {
|
||||||
|
return ListView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: [
|
||||||
|
if (stats != null) ...[
|
||||||
|
Text(
|
||||||
|
l10n.radioStats_noiseFloor(stats.noiseFloorDbm),
|
||||||
|
style: tt.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(l10n.radioStats_lastRssi(stats.lastRssiDbm)),
|
||||||
|
Text(
|
||||||
|
l10n.radioStats_lastSnr(
|
||||||
|
stats.lastSnrDb.toStringAsFixed(1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(l10n.radioStats_txAir(stats.txAirSecs)),
|
||||||
|
Text(l10n.radioStats_rxAir(stats.rxAirSecs)),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
] else
|
||||||
|
Text(l10n.radioStats_waiting),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
SizedBox(
|
||||||
|
height: 200,
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: _NoiseChartPainter(
|
||||||
|
samples: List<double>.from(_noiseHistory),
|
||||||
|
colorScheme: scheme,
|
||||||
|
textTheme: tt,
|
||||||
|
),
|
||||||
|
child: const SizedBox.expand(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
l10n.radioStats_chartCaption,
|
||||||
|
style: tt.bodySmall?.copyWith(
|
||||||
|
color: scheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NoiseChartPainter extends CustomPainter {
|
||||||
|
final List<double> samples;
|
||||||
|
final ColorScheme colorScheme;
|
||||||
|
final TextTheme textTheme;
|
||||||
|
|
||||||
|
_NoiseChartPainter({
|
||||||
|
required this.samples,
|
||||||
|
required this.colorScheme,
|
||||||
|
required this.textTheme,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final bg = Paint()..color = colorScheme.surfaceContainerHighest;
|
||||||
|
final border = Paint()
|
||||||
|
..color = colorScheme.outlineVariant
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 1;
|
||||||
|
final grid = Paint()
|
||||||
|
..color = colorScheme.outlineVariant.withValues(alpha: 0.5)
|
||||||
|
..strokeWidth = 1;
|
||||||
|
final line = Paint()
|
||||||
|
..color = colorScheme.primary
|
||||||
|
..strokeWidth = 2
|
||||||
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
|
final rect = Rect.fromLTWH(0, 0, size.width, size.height);
|
||||||
|
canvas.drawRRect(
|
||||||
|
RRect.fromRectAndRadius(rect, const Radius.circular(8)),
|
||||||
|
bg,
|
||||||
|
);
|
||||||
|
canvas.drawRRect(
|
||||||
|
RRect.fromRectAndRadius(rect, const Radius.circular(8)),
|
||||||
|
border,
|
||||||
|
);
|
||||||
|
|
||||||
|
const padL = 40.0;
|
||||||
|
const padR = 8.0;
|
||||||
|
const padT = 8.0;
|
||||||
|
const padB = 24.0;
|
||||||
|
final chart = Rect.fromLTRB(
|
||||||
|
padL,
|
||||||
|
padT,
|
||||||
|
size.width - padR,
|
||||||
|
size.height - padB,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (var i = 0; i <= 4; i++) {
|
||||||
|
final y = chart.top + (chart.height * i / 4);
|
||||||
|
canvas.drawLine(Offset(chart.left, y), Offset(chart.right, y), grid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (samples.length < 2) {
|
||||||
|
final tp = TextPainter(
|
||||||
|
text: TextSpan(
|
||||||
|
text: '—',
|
||||||
|
style: textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
)..layout();
|
||||||
|
tp.paint(
|
||||||
|
canvas,
|
||||||
|
Offset(chart.left + 4, chart.top + chart.height / 2 - tp.height / 2),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
double minV = samples.reduce((a, b) => a < b ? a : b);
|
||||||
|
double maxV = samples.reduce((a, b) => a > b ? a : b);
|
||||||
|
if ((maxV - minV).abs() < 1) {
|
||||||
|
minV -= 2;
|
||||||
|
maxV += 2;
|
||||||
|
}
|
||||||
|
final span = maxV - minV;
|
||||||
|
|
||||||
|
for (var i = 0; i <= 2; i++) {
|
||||||
|
final v = maxV - span * i / 2;
|
||||||
|
final tp = _yAxisLabel(v);
|
||||||
|
final y = chart.top + (chart.height * i / 2) - tp.height / 2;
|
||||||
|
tp.paint(canvas, Offset(4, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
final path = Path();
|
||||||
|
for (var i = 0; i < samples.length; i++) {
|
||||||
|
final x = chart.left + (chart.width * i / (samples.length - 1));
|
||||||
|
final t = (samples[i] - minV) / span;
|
||||||
|
final y = chart.bottom - t * chart.height;
|
||||||
|
if (i == 0) {
|
||||||
|
path.moveTo(x, y);
|
||||||
|
} else {
|
||||||
|
path.lineTo(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
canvas.drawPath(path, line);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant _NoiseChartPainter oldDelegate) {
|
||||||
|
return oldDelegate.samples.length != samples.length ||
|
||||||
|
oldDelegate.colorScheme != colorScheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextPainter _yAxisLabel(double v) {
|
||||||
|
final tp = TextPainter(
|
||||||
|
text: TextSpan(
|
||||||
|
text: v.round().toString(),
|
||||||
|
style: textTheme.labelSmall?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
)..layout();
|
||||||
|
return tp;
|
||||||
|
}
|
||||||
|
}
|
||||||
+713
-392
File diff suppressed because it is too large
Load Diff
@@ -1,280 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
import '../connector/meshcore_connector.dart';
|
|
||||||
import '../l10n/l10n.dart';
|
|
||||||
import '../utils/dialog_utils.dart';
|
|
||||||
import '../utils/disconnect_navigation_mixin.dart';
|
|
||||||
import '../utils/route_transitions.dart';
|
|
||||||
import '../widgets/quick_switch_bar.dart';
|
|
||||||
import 'channels_screen.dart';
|
|
||||||
import 'contacts_screen.dart';
|
|
||||||
import 'map_screen.dart';
|
|
||||||
import 'settings_screen.dart';
|
|
||||||
|
|
||||||
/// Main hub screen after connecting to a MeshCore device
|
|
||||||
class DeviceScreen extends StatefulWidget {
|
|
||||||
const DeviceScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<DeviceScreen> createState() => _DeviceScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DeviceScreenState extends State<DeviceScreen>
|
|
||||||
with DisconnectNavigationMixin {
|
|
||||||
bool _showBatteryVoltage = false;
|
|
||||||
int _quickIndex = 0;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Consumer<MeshCoreConnector>(
|
|
||||||
builder: (context, connector, child) {
|
|
||||||
// Auto-navigate back to scanner if disconnected
|
|
||||||
if (!checkConnectionAndNavigate(connector)) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
return PopScope(
|
|
||||||
canPop: false,
|
|
||||||
child: Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
leading: _buildBatteryIndicator(connector, context),
|
|
||||||
titleSpacing: 16,
|
|
||||||
centerTitle: false,
|
|
||||||
title: _buildAppBarTitle(connector, theme),
|
|
||||||
automaticallyImplyLeading: false,
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.bluetooth_disabled),
|
|
||||||
tooltip: context.l10n.common_disconnect,
|
|
||||||
onPressed: () => _disconnect(context, connector),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.tune),
|
|
||||||
tooltip: context.l10n.common_settings,
|
|
||||||
onPressed: () => Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => const SettingsScreen(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: SafeArea(
|
|
||||||
child: ListView(
|
|
||||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
|
|
||||||
children: [
|
|
||||||
_buildConnectionCard(connector, context),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_buildSectionLabel(theme, context.l10n.device_quickSwitch),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
_buildQuickSwitchBar(context),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAppBarTitle(MeshCoreConnector connector, ThemeData theme) {
|
|
||||||
final colorScheme = theme.colorScheme;
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
context.l10n.device_meshcore,
|
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
letterSpacing: 0.8,
|
|
||||||
color: colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
connector.deviceDisplayName,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSectionLabel(ThemeData theme, String text) {
|
|
||||||
return Text(
|
|
||||||
text,
|
|
||||||
style: theme.textTheme.titleSmall?.copyWith(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
letterSpacing: 0.6,
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildConnectionCard(
|
|
||||||
MeshCoreConnector connector,
|
|
||||||
BuildContext context,
|
|
||||||
) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final colorScheme = theme.colorScheme;
|
|
||||||
|
|
||||||
return Card(
|
|
||||||
elevation: 0,
|
|
||||||
color: colorScheme.surfaceContainerHighest,
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
CircleAvatar(
|
|
||||||
radius: 24,
|
|
||||||
backgroundColor: colorScheme.primaryContainer,
|
|
||||||
child: Icon(
|
|
||||||
Icons.wifi_tethering_rounded,
|
|
||||||
color: colorScheme.onPrimaryContainer,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
connector.deviceDisplayName,
|
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
connector.deviceIdLabel,
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Wrap(
|
|
||||||
spacing: 8,
|
|
||||||
runSpacing: 8,
|
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
|
||||||
children: [
|
|
||||||
Chip(
|
|
||||||
avatar: Icon(
|
|
||||||
Icons.check_circle,
|
|
||||||
size: 18,
|
|
||||||
color: colorScheme.onSecondaryContainer,
|
|
||||||
),
|
|
||||||
label: Text(context.l10n.common_connected),
|
|
||||||
backgroundColor: colorScheme.secondaryContainer,
|
|
||||||
labelStyle: theme.textTheme.labelMedium?.copyWith(
|
|
||||||
color: colorScheme.onSecondaryContainer,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
),
|
|
||||||
_buildBatteryIndicator(connector, context),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildQuickSwitchBar(BuildContext context) {
|
|
||||||
return QuickSwitchBar(
|
|
||||||
selectedIndex: _quickIndex,
|
|
||||||
onDestinationSelected: (index) {
|
|
||||||
_openQuickDestination(index, context);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildBatteryIndicator(
|
|
||||||
MeshCoreConnector connector,
|
|
||||||
BuildContext context,
|
|
||||||
) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final colorScheme = theme.colorScheme;
|
|
||||||
final percent = connector.batteryPercent;
|
|
||||||
final millivolts = connector.batteryMillivolts;
|
|
||||||
final percentLabel = percent != null ? '$percent%' : '--%';
|
|
||||||
final voltageLabel = millivolts == null
|
|
||||||
? '-- V'
|
|
||||||
: '${(millivolts / 1000.0).toStringAsFixed(2)} V';
|
|
||||||
final displayLabel = _showBatteryVoltage ? voltageLabel : percentLabel;
|
|
||||||
final icon = _batteryIcon(percent);
|
|
||||||
|
|
||||||
return ActionChip(
|
|
||||||
avatar: Icon(icon, size: 16, color: colorScheme.onSecondaryContainer),
|
|
||||||
label: Text(displayLabel),
|
|
||||||
labelStyle: theme.textTheme.labelMedium?.copyWith(
|
|
||||||
color: colorScheme.onSecondaryContainer,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
backgroundColor: colorScheme.secondaryContainer,
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
_showBatteryVoltage = !_showBatteryVoltage;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
IconData _batteryIcon(int? percent) {
|
|
||||||
if (percent == null) return Icons.battery_unknown;
|
|
||||||
if (percent <= 15) return Icons.battery_alert;
|
|
||||||
return Icons.battery_full;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _openQuickDestination(int index, BuildContext context) {
|
|
||||||
if (_quickIndex != index) {
|
|
||||||
setState(() {
|
|
||||||
_quickIndex = index;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
switch (index) {
|
|
||||||
case 0:
|
|
||||||
Navigator.pushReplacement(
|
|
||||||
context,
|
|
||||||
buildQuickSwitchRoute(const ContactsScreen(hideBackButton: true)),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
Navigator.pushReplacement(
|
|
||||||
context,
|
|
||||||
buildQuickSwitchRoute(const ChannelsScreen(hideBackButton: true)),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
Navigator.pushReplacement(
|
|
||||||
context,
|
|
||||||
buildQuickSwitchRoute(const MapScreen(hideBackButton: true)),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _disconnect(
|
|
||||||
BuildContext context,
|
|
||||||
MeshCoreConnector connector,
|
|
||||||
) async {
|
|
||||||
await showDisconnectDialog(context, connector);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,481 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime _resolveLastSeen(Contact contact) {
|
||||||
|
if (contact.type != advTypeChat) return contact.lastSeen;
|
||||||
|
return contact.lastMessageAt.isAfter(contact.lastSeen)
|
||||||
|
? contact.lastMessageAt
|
||||||
|
: contact.lastSeen;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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,
|
||||||
|
),
|
||||||
|
// Clamp text scaling in trailing section to prevent overflow while
|
||||||
|
// maintaining accessibility. Primary content (title/subtitle) scales normally.
|
||||||
|
trailing: MediaQuery(
|
||||||
|
data: MediaQuery.of(context).copyWith(
|
||||||
|
textScaler: TextScaler.linear(
|
||||||
|
MediaQuery.textScalerOf(
|
||||||
|
context,
|
||||||
|
).scale(1.0).clamp(1.0, 1.3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 120,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_formatLastSeen(
|
||||||
|
context,
|
||||||
|
_resolveLastSeen(contact),
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (contact.hasLocation)
|
||||||
|
Icon(
|
||||||
|
Icons.location_on,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.grey[400],
|
||||||
|
),
|
||||||
|
if (contact.rawPacket != null)
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
if (contact.rawPacket != null)
|
||||||
|
Icon(
|
||||||
|
Icons.cell_tower,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.grey[400],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ import '../l10n/app_localizations.dart';
|
|||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../services/app_settings_service.dart';
|
import '../services/app_settings_service.dart';
|
||||||
import '../services/map_tile_cache_service.dart';
|
import '../services/map_tile_cache_service.dart';
|
||||||
|
import '../widgets/adaptive_app_bar_title.dart';
|
||||||
|
|
||||||
class MapCacheScreen extends StatefulWidget {
|
class MapCacheScreen extends StatefulWidget {
|
||||||
const MapCacheScreen({super.key});
|
const MapCacheScreen({super.key});
|
||||||
@@ -224,7 +225,10 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
|||||||
: (_completedTiles / _estimatedTiles).clamp(0.0, 1.0).toDouble();
|
: (_completedTiles / _estimatedTiles).clamp(0.0, 1.0).toDouble();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: Text(l10n.mapCache_title), centerTitle: true),
|
appBar: AppBar(
|
||||||
|
title: AdaptiveAppBarTitle(l10n.mapCache_title),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|||||||
+770
-71
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:meshcore_open/utils/app_logger.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
@@ -11,28 +12,28 @@ import '../services/repeater_command_service.dart';
|
|||||||
import '../widgets/path_management_dialog.dart';
|
import '../widgets/path_management_dialog.dart';
|
||||||
import '../widgets/snr_indicator.dart';
|
import '../widgets/snr_indicator.dart';
|
||||||
|
|
||||||
class NeighboursScreen extends StatefulWidget {
|
class NeighborsScreen extends StatefulWidget {
|
||||||
final Contact repeater;
|
final Contact repeater;
|
||||||
final String password;
|
final String password;
|
||||||
|
|
||||||
const NeighboursScreen({
|
const NeighborsScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.repeater,
|
required this.repeater,
|
||||||
required this.password,
|
required this.password,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<NeighboursScreen> createState() => _NeighboursScreenState();
|
State<NeighborsScreen> createState() => _NeighborsScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NeighboursScreenState extends State<NeighboursScreen> {
|
class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||||
static const int _reqNeighboursKeyLen = 4;
|
static const int _reqNeighborsKeyLen = 4;
|
||||||
static const int _statusPayloadOffset = 8;
|
static const int _statusPayloadOffset = 8;
|
||||||
static const int _statusStatsSize = 52;
|
static const int _statusStatsSize = 52;
|
||||||
static const int _statusResponseBytes =
|
static const int _statusResponseBytes =
|
||||||
_statusPayloadOffset + _statusStatsSize;
|
_statusPayloadOffset + _statusStatsSize;
|
||||||
Uint8List _tagData = Uint8List(4);
|
Uint8List _tagData = Uint8List(4);
|
||||||
int _neighbourCount = 0;
|
int _neighborCount = 0;
|
||||||
|
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
bool _isLoaded = false;
|
bool _isLoaded = false;
|
||||||
@@ -41,7 +42,25 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
StreamSubscription<Uint8List>? _frameSubscription;
|
StreamSubscription<Uint8List>? _frameSubscription;
|
||||||
RepeaterCommandService? _commandService;
|
RepeaterCommandService? _commandService;
|
||||||
PathSelection? _pendingStatusSelection;
|
PathSelection? _pendingStatusSelection;
|
||||||
List<Map<String, dynamic>>? _parsedNeighbours;
|
List<Map<String, dynamic>>? _parsedNeighbors;
|
||||||
|
|
||||||
|
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() {
|
||||||
@@ -49,7 +68,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
_commandService = RepeaterCommandService(connector);
|
_commandService = RepeaterCommandService(connector);
|
||||||
_setupMessageListener();
|
_setupMessageListener();
|
||||||
_loadNeighbours();
|
_loadNeighbors();
|
||||||
_hasData = false;
|
_hasData = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,13 +81,12 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
|
|
||||||
if (frame[0] == respCodeSent) {
|
if (frame[0] == respCodeSent) {
|
||||||
_tagData = frame.sublist(2, 6);
|
_tagData = frame.sublist(2, 6);
|
||||||
//_timeEstment = frame.buffer.asByteData().getUint32(6, Endian.little);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's a binary response
|
// Check if it's a binary response
|
||||||
if (frame[0] == pushCodeBinaryResponse &&
|
if (frame[0] == pushCodeBinaryResponse &&
|
||||||
listEquals(frame.sublist(2, 6), _tagData)) {
|
listEquals(frame.sublist(2, 6), _tagData)) {
|
||||||
_handleNeighboursResponse(connector, frame.sublist(6));
|
_handleNeighborsResponse(connector, frame.sublist(6));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -91,75 +109,79 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
return '${h}h ${m2}m';
|
return '${h}h ${m2}m';
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<Map<String, dynamic>> parseNeighboursData(
|
static List<Map<String, dynamic>> parseNeighborsData(
|
||||||
BufferReader buffer,
|
BufferReader buffer,
|
||||||
int resultsCount,
|
int resultsCount,
|
||||||
) {
|
) {
|
||||||
final Map<int, Map<String, dynamic>> neighbours = {};
|
final Map<int, Map<String, dynamic>> neighbors = {};
|
||||||
for (var i = 0; i < resultsCount; i++) {
|
try {
|
||||||
final neighbourData = neighbours.putIfAbsent(
|
for (var i = 0; i < resultsCount; i++) {
|
||||||
i,
|
final neighborData = neighbors.putIfAbsent(
|
||||||
() => {
|
i,
|
||||||
'contact': null,
|
() => {
|
||||||
'publicKey': <Uint8List>{},
|
'contact': null,
|
||||||
'lastHeard': <int>{},
|
'publicKey': <Uint8List>{},
|
||||||
'snr': <double>{},
|
'lastHeard': <int>{},
|
||||||
},
|
'snr': <double>{},
|
||||||
);
|
},
|
||||||
neighbourData['publicKey'] = buffer.readBytes(_reqNeighboursKeyLen);
|
);
|
||||||
neighbourData['lastHeard'] = buffer.readUInt32LE();
|
neighborData['publicKey'] = buffer.readBytes(_reqNeighborsKeyLen);
|
||||||
neighbourData['snr'] = buffer.readInt8() / 4.0;
|
neighborData['lastHeard'] = buffer.readUInt32LE();
|
||||||
}
|
neighborData['snr'] = buffer.readInt8() / 4.0;
|
||||||
|
|
||||||
return neighbours.values.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleNeighboursResponse(MeshCoreConnector connector, Uint8List frame) {
|
|
||||||
final buffer = BufferReader(frame);
|
|
||||||
final neighbourCount = buffer.readUInt16LE();
|
|
||||||
final parsedNeighbours = parseNeighboursData(buffer, buffer.readUInt16LE());
|
|
||||||
connector.contacts.where((c) => c.type == advTypeRepeater).forEach((
|
|
||||||
repeater,
|
|
||||||
) {
|
|
||||||
for (var neighbourData in parsedNeighbours) {
|
|
||||||
final publicKey = neighbourData['publicKey'];
|
|
||||||
if (listEquals(
|
|
||||||
repeater.publicKey.sublist(0, _reqNeighboursKeyLen),
|
|
||||||
publicKey,
|
|
||||||
)) {
|
|
||||||
neighbourData['contact'] = repeater;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
setState(() {
|
return neighbors.values.toList();
|
||||||
_parsedNeighbours = parsedNeighbours;
|
} catch (e) {
|
||||||
_neighbourCount = neighbourCount;
|
appLogger.error(
|
||||||
});
|
'Error parsing neighbors data: $e',
|
||||||
|
tag: 'NeighborsScreen',
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
);
|
||||||
SnackBar(
|
return [];
|
||||||
content: Text(context.l10n.neighbors_receivedData),
|
}
|
||||||
backgroundColor: Colors.green,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
_statusTimeout?.cancel();
|
|
||||||
if (!mounted) return;
|
|
||||||
setState(() {
|
|
||||||
_isLoading = false;
|
|
||||||
_isLoaded = true;
|
|
||||||
_hasData = true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Contact _resolveRepeater(MeshCoreConnector connector) {
|
void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) {
|
||||||
return connector.contacts.firstWhere(
|
final buffer = BufferReader(frame);
|
||||||
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
|
final contacts = connector.allContactsUnfiltered;
|
||||||
orElse: () => widget.repeater,
|
try {
|
||||||
);
|
final neighborCount = buffer.readUInt16LE();
|
||||||
|
final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE());
|
||||||
|
contacts.where((c) => c.type == advTypeRepeater).forEach((repeater) {
|
||||||
|
for (var neighborData in parsedNeighbors) {
|
||||||
|
final publicKey = neighborData['publicKey'];
|
||||||
|
if (listEquals(
|
||||||
|
repeater.publicKey.sublist(0, _reqNeighborsKeyLen),
|
||||||
|
publicKey,
|
||||||
|
)) {
|
||||||
|
neighborData['contact'] = repeater;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_parsedNeighbors = parsedNeighbors;
|
||||||
|
_neighborCount = neighborCount;
|
||||||
|
});
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(context.l10n.neighbors_receivedData),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_statusTimeout?.cancel();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_isLoaded = true;
|
||||||
|
_hasData = true;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
appLogger.error('Error handling neighbors response: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadNeighbours() async {
|
Future<void> _loadNeighbors() async {
|
||||||
if (_commandService == null) return;
|
if (_commandService == null) return;
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -172,17 +194,17 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
final selection = await connector.preparePathForContactSend(repeater);
|
final selection = await connector.preparePathForContactSend(repeater);
|
||||||
_pendingStatusSelection = selection;
|
_pendingStatusSelection = selection;
|
||||||
|
|
||||||
//[version][number of requested neighbours][offset_16bit][order by][len of public key]
|
//[version][number of requested neighbors][offset_16bit][order by][len of public key]
|
||||||
final frame = buildSendBinaryReq(
|
final frame = buildSendBinaryReq(
|
||||||
repeater.publicKey,
|
repeater.publicKey,
|
||||||
payload: Uint8List.fromList([
|
payload: Uint8List.fromList([
|
||||||
reqTypeGetNeighbours,
|
reqTypeGetNeighbors,
|
||||||
0x00,
|
0x00,
|
||||||
0x0F,
|
0x0F,
|
||||||
0x00,
|
0x00,
|
||||||
0x00,
|
0x00,
|
||||||
0x00,
|
0x00,
|
||||||
_reqNeighboursKeyLen,
|
_reqNeighborsKeyLen,
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
await connector.sendFrame(frame);
|
await connector.sendFrame(frame);
|
||||||
@@ -258,7 +280,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
l10n.neighbors_repeatersNeighbours,
|
l10n.neighbors_repeatersNeighbors,
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
@@ -345,7 +367,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: const Icon(Icons.refresh),
|
: const Icon(Icons.refresh),
|
||||||
onPressed: _isLoading ? null : _loadNeighbours,
|
onPressed: _isLoading ? null : _loadNeighbors,
|
||||||
tooltip: l10n.repeater_refresh,
|
tooltip: l10n.repeater_refresh,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -353,13 +375,13 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
child: RefreshIndicator(
|
child: RefreshIndicator(
|
||||||
onRefresh: _loadNeighbours,
|
onRefresh: _loadNeighbors,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
children: [
|
children: [
|
||||||
if (!_isLoaded &&
|
if (!_isLoaded &&
|
||||||
!_hasData &&
|
!_hasData &&
|
||||||
(_parsedNeighbours == null || _parsedNeighbours!.isEmpty))
|
(_parsedNeighbors == null || _parsedNeighbors!.isEmpty))
|
||||||
Center(
|
Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
l10n.neighbors_noData,
|
l10n.neighbors_noData,
|
||||||
@@ -368,10 +390,9 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
),
|
),
|
||||||
if (_isLoaded ||
|
if (_isLoaded ||
|
||||||
_hasData &&
|
_hasData &&
|
||||||
!(_parsedNeighbours == null ||
|
!(_parsedNeighbors == null || _parsedNeighbors!.isEmpty))
|
||||||
_parsedNeighbours!.isEmpty))
|
_buildNeighborsInfoCard(
|
||||||
_buildNeighboursInfoCard(
|
"${l10n.repeater_neighbors} - $_neighborCount",
|
||||||
"${l10n.repeater_neighbours} - $_neighbourCount",
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -380,7 +401,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildNeighboursInfoCard(String title) {
|
Widget _buildNeighborsInfoCard(String title) {
|
||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
return Card(
|
return Card(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -405,7 +426,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
for (final entry in _parsedNeighbours!.asMap().entries)
|
for (final entry in _parsedNeighbors!.asMap().entries)
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
entry.value['contact'] != null
|
entry.value['contact'] != null
|
||||||
? entry.value['contact'].name
|
? entry.value['contact'].name
|
||||||
@@ -430,6 +451,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
double snr,
|
double snr,
|
||||||
int spreadingFactor,
|
int spreadingFactor,
|
||||||
) {
|
) {
|
||||||
|
final snrUi = snrUiFromSNR(snr, spreadingFactor);
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -443,9 +465,15 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
|||||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||||
),
|
),
|
||||||
subtitle: Text(value),
|
subtitle: Text(value),
|
||||||
trailing: SNRIcon(
|
trailing: Column(
|
||||||
snr: snr,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
snrLevels: getSNRfromSF(spreadingFactor),
|
children: [
|
||||||
|
Icon(snrUi.icon, color: snrUi.color, size: 18.0),
|
||||||
|
Text(
|
||||||
|
snrUi.text,
|
||||||
|
style: TextStyle(fontSize: 10, color: snrUi.color),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
+521
-153
@@ -8,8 +8,11 @@ import 'package:latlong2/latlong.dart';
|
|||||||
import 'package:meshcore_open/connector/meshcore_connector.dart';
|
import 'package:meshcore_open/connector/meshcore_connector.dart';
|
||||||
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
||||||
import 'package:meshcore_open/l10n/l10n.dart';
|
import 'package:meshcore_open/l10n/l10n.dart';
|
||||||
|
import 'package:meshcore_open/models/app_settings.dart';
|
||||||
import 'package:meshcore_open/models/contact.dart';
|
import 'package:meshcore_open/models/contact.dart';
|
||||||
|
import 'package:meshcore_open/services/app_settings_service.dart';
|
||||||
import 'package:meshcore_open/services/map_tile_cache_service.dart';
|
import 'package:meshcore_open/services/map_tile_cache_service.dart';
|
||||||
|
import 'package:meshcore_open/utils/app_logger.dart';
|
||||||
import 'package:meshcore_open/widgets/snr_indicator.dart';
|
import 'package:meshcore_open/widgets/snr_indicator.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -26,13 +29,16 @@ double getPathDistanceMeters(List<LatLng> points) {
|
|||||||
return distanceMeters;
|
return distanceMeters;
|
||||||
}
|
}
|
||||||
|
|
||||||
String formatDistance(double distanceMeters) {
|
String formatDistance(double distanceMeters, {required bool isImperial}) {
|
||||||
return '(${(distanceMeters / 1609.34).toStringAsFixed(2)} Miles / ${(distanceMeters / 1000).toStringAsFixed(2)} Km)';
|
if (isImperial) {
|
||||||
|
return '(${(distanceMeters / 1609.34).toStringAsFixed(2)} mi)';
|
||||||
|
}
|
||||||
|
return '(${(distanceMeters / 1000).toStringAsFixed(2)} km)';
|
||||||
}
|
}
|
||||||
|
|
||||||
class PathTraceData {
|
class PathTraceData {
|
||||||
final Uint8List pathData;
|
final Uint8List pathData;
|
||||||
final Uint8List snrData;
|
final List<double> snrData;
|
||||||
final Map<int, Contact> pathContacts;
|
final Map<int, Contact> pathContacts;
|
||||||
|
|
||||||
PathTraceData({
|
PathTraceData({
|
||||||
@@ -45,15 +51,23 @@ class PathTraceData {
|
|||||||
class PathTraceMapScreen extends StatefulWidget {
|
class PathTraceMapScreen extends StatefulWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final Uint8List path;
|
final Uint8List path;
|
||||||
final bool flipPathRound;
|
final int? repeaterId;
|
||||||
final bool reversePathRound;
|
final bool flipPathAround;
|
||||||
|
final bool reversePathAround;
|
||||||
|
final Contact? targetContact;
|
||||||
|
final int pathHashByteWidth;
|
||||||
|
final List<Contact>? pathContacts;
|
||||||
|
|
||||||
const PathTraceMapScreen({
|
const PathTraceMapScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.path,
|
required this.path,
|
||||||
this.flipPathRound = false,
|
this.repeaterId,
|
||||||
this.reversePathRound = false,
|
this.flipPathAround = false,
|
||||||
|
this.reversePathAround = false,
|
||||||
|
this.targetContact,
|
||||||
|
this.pathHashByteWidth = pathHashSize,
|
||||||
|
this.pathContacts,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -61,6 +75,10 @@ class PathTraceMapScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||||
|
static const double _labelZoomThreshold = 8.5;
|
||||||
|
//miles to meters conversion for filtering out repeaters that are too far from the last known GPS hop to be a likely match, to avoid false matches that throw off the inferred positions of other hops in the path
|
||||||
|
static const double _maxRepeaterMatchDistanceMeters = 40 * 1609.344;
|
||||||
|
|
||||||
StreamSubscription<Uint8List>? _frameSubscription;
|
StreamSubscription<Uint8List>? _frameSubscription;
|
||||||
Timer? _timeoutTimer;
|
Timer? _timeoutTimer;
|
||||||
|
|
||||||
@@ -68,6 +86,11 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
bool _failed2Loaded = false;
|
bool _failed2Loaded = false;
|
||||||
bool _hasData = false;
|
bool _hasData = false;
|
||||||
PathTraceData? _traceData;
|
PathTraceData? _traceData;
|
||||||
|
// Inferred positions for hops that have no GPS location, keyed by hop byte.
|
||||||
|
Map<int, LatLng> _inferredHopPositions = {};
|
||||||
|
// Endpoint position for the target contact (GPS or guessed).
|
||||||
|
LatLng? _targetContactPosition;
|
||||||
|
bool _targetContactIsGuessed = false;
|
||||||
List<LatLng> _points = <LatLng>[];
|
List<LatLng> _points = <LatLng>[];
|
||||||
List<Polyline> _polylines = [];
|
List<Polyline> _polylines = [];
|
||||||
LatLng? _initialCenter = LatLng(0, 0);
|
LatLng? _initialCenter = LatLng(0, 0);
|
||||||
@@ -75,6 +98,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
LatLngBounds? _bounds;
|
LatLngBounds? _bounds;
|
||||||
ValueKey<String> _mapKey = const ValueKey('initial');
|
ValueKey<String> _mapKey = const ValueKey('initial');
|
||||||
double _pathDistanceMeters = 0.0;
|
double _pathDistanceMeters = 0.0;
|
||||||
|
bool _showNodeLabels = true;
|
||||||
|
Contact? _targetContact;
|
||||||
|
|
||||||
String _formatPathPrefixes(Uint8List pathBytes) {
|
String _formatPathPrefixes(Uint8List pathBytes) {
|
||||||
return pathBytes
|
return pathBytes
|
||||||
@@ -96,14 +121,42 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint8List addReturnpath(Uint8List pathBytes) {
|
Uint8List buildPath(Uint8List pathBytes) {
|
||||||
Uint8List? traceBytes;
|
Uint8List traceBytes;
|
||||||
final len = (pathBytes.length + pathBytes.length - 1);
|
|
||||||
traceBytes = Uint8List(len);
|
if (pathBytes.isEmpty) {
|
||||||
for (int i = 0; i < pathBytes.length; i++) {
|
final pk = widget.targetContact?.publicKey;
|
||||||
traceBytes[i] = pathBytes[i];
|
final n = widget.pathHashByteWidth.clamp(1, pubKeySize);
|
||||||
if (i < pathBytes.length - 1) {
|
if (pk != null && pk.length >= n) {
|
||||||
traceBytes[len - 1 - i] = pathBytes[i];
|
return Uint8List.fromList(pk.sublist(0, n));
|
||||||
|
}
|
||||||
|
traceBytes = Uint8List(1);
|
||||||
|
traceBytes[0] = pk?[0] ?? 0;
|
||||||
|
return traceBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget.targetContact?.type == advTypeRepeater ||
|
||||||
|
widget.targetContact?.type == advTypeRoom) {
|
||||||
|
final len = (pathBytes.length + pathBytes.length + 1);
|
||||||
|
traceBytes = Uint8List(len);
|
||||||
|
traceBytes[pathBytes.length] = widget.targetContact?.publicKey[0] ?? 0;
|
||||||
|
for (int i = 0; i < pathBytes.length; i++) {
|
||||||
|
traceBytes[i] = pathBytes[i];
|
||||||
|
if (i < pathBytes.length) {
|
||||||
|
traceBytes[len - 1 - i] = pathBytes[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (pathBytes.length < 2) {
|
||||||
|
return pathBytes[0] == 0 ? Uint8List(0) : pathBytes;
|
||||||
|
}
|
||||||
|
final len = (pathBytes.length + pathBytes.length - 1);
|
||||||
|
traceBytes = Uint8List(len);
|
||||||
|
for (int i = 0; i < pathBytes.length; i++) {
|
||||||
|
traceBytes[i] = pathBytes[i];
|
||||||
|
if (i < pathBytes.length - 1) {
|
||||||
|
traceBytes[len - 1 - i] = pathBytes[i];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return traceBytes;
|
return traceBytes;
|
||||||
@@ -117,17 +170,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(
|
||||||
@@ -146,42 +199,57 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
_frameSubscription = connector.receivedFrames.listen((frame) {
|
_frameSubscription = connector.receivedFrames.listen((frame) {
|
||||||
if (frame.isEmpty) return;
|
if (frame.isEmpty) return;
|
||||||
final frameBuffer = BufferReader(frame);
|
final frameBuffer = BufferReader(frame);
|
||||||
final code = frameBuffer.readUInt8();
|
try {
|
||||||
|
final code = frameBuffer.readUInt8();
|
||||||
|
|
||||||
if (code == respCodeSent) {
|
if (code == respCodeSent) {
|
||||||
frameBuffer.skipBytes(1); //reserved
|
frameBuffer.skipBytes(1); //reserved
|
||||||
tagData = frameBuffer.readBytes(4);
|
tagData = frameBuffer.readBytes(4);
|
||||||
final timeoutSeconds = frameBuffer.readUInt32LE();
|
final timeoutMilliseconds = frameBuffer.readUInt32LE();
|
||||||
|
|
||||||
// Start timeout timer for trace response
|
// Start timeout timer for trace response
|
||||||
_timeoutTimer?.cancel();
|
_timeoutTimer?.cancel();
|
||||||
_timeoutTimer = Timer(Duration(milliseconds: timeoutSeconds), () {
|
_timeoutTimer = Timer(
|
||||||
|
Duration(milliseconds: timeoutMilliseconds),
|
||||||
|
() {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_failed2Loaded = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code == respCodeErr) {
|
||||||
|
_timeoutTimer?.cancel();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
_failed2Loaded = true;
|
_failed2Loaded = true;
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (code == respCodeErr) {
|
// Check if it's a binary response
|
||||||
|
if (frame.length > 8 &&
|
||||||
|
code == pushCodeTraceData &&
|
||||||
|
listEquals(frame.sublist(4, 8), tagData)) {
|
||||||
|
_timeoutTimer?.cancel();
|
||||||
|
if (!mounted) return;
|
||||||
|
frameBuffer.skipBytes(3); //reserved + path length + flag
|
||||||
|
if (listEquals(frameBuffer.readBytes(4), tagData)) {
|
||||||
|
_handleTraceResponse(frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
_timeoutTimer?.cancel();
|
_timeoutTimer?.cancel();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
_failed2Loaded = true;
|
_failed2Loaded = true;
|
||||||
});
|
});
|
||||||
}
|
// Handle any parsing errors gracefully
|
||||||
// Check if it's a binary response
|
appLogger.error('Error parsing frame: $e', tag: 'PathTraceMapScreen');
|
||||||
if (frame.length > 8 &&
|
|
||||||
code == pushCodeTraceData &&
|
|
||||||
listEquals(frame.sublist(4, 8), tagData)) {
|
|
||||||
_timeoutTimer?.cancel();
|
|
||||||
if (!mounted) return;
|
|
||||||
frameBuffer.skipBytes(3); //reserved + path length + flag
|
|
||||||
if (listEquals(frameBuffer.readBytes(4), tagData)) {
|
|
||||||
_handleTraceResponse(frame);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -190,69 +258,217 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||||
|
|
||||||
final buffer = BufferReader(frame);
|
final buffer = BufferReader(frame);
|
||||||
buffer.skipBytes(2); // Skip push code and reserved byte
|
try {
|
||||||
int pathLength = buffer.readUInt8();
|
buffer.skipBytes(2); // Skip push code and reserved byte
|
||||||
buffer.skipBytes(5); // Skip Flag byte and tag data
|
int pathLength = buffer.readUInt8();
|
||||||
buffer.skipBytes(4); // Skip auth code
|
buffer.skipBytes(5); // Skip Flag byte and tag data
|
||||||
Uint8List pathData = buffer.readBytes(pathLength);
|
buffer.skipBytes(4); // Skip auth code
|
||||||
Uint8List snrData = buffer.readRemainingBytes();
|
Uint8List pathData = buffer.readBytes(pathLength);
|
||||||
|
List<double> snrData = buffer
|
||||||
|
.readRemainingBytes()
|
||||||
|
.map((snr) => snr.toSigned(8).toDouble() / 4)
|
||||||
|
.toList();
|
||||||
|
|
||||||
Map<int, Contact> pathContacts = {};
|
Map<int, Contact> pathContacts = {};
|
||||||
|
Contact lastContact = Contact(
|
||||||
|
path: Uint8List(0),
|
||||||
|
pathLength: 0,
|
||||||
|
publicKey: connector.selfPublicKey ?? Uint8List(0),
|
||||||
|
name: context.l10n.pathTrace_you,
|
||||||
|
type: advTypeChat,
|
||||||
|
latitude: connector.selfLatitude,
|
||||||
|
longitude: connector.selfLongitude,
|
||||||
|
lastSeen: DateTime.now(),
|
||||||
|
);
|
||||||
|
if (widget.pathContacts != null) {
|
||||||
|
pathContacts = {for (var c in widget.pathContacts!) c.publicKey[0]: c};
|
||||||
|
} else {
|
||||||
|
final contacts = connector.allContactsUnfiltered;
|
||||||
|
contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
|
||||||
|
if (lastContact.latitude != null &&
|
||||||
|
lastContact.longitude != null &&
|
||||||
|
repeater.hasLocation &&
|
||||||
|
lastContact.hasLocation &&
|
||||||
|
Distance().distance(
|
||||||
|
LatLng(lastContact.latitude!, lastContact.longitude!),
|
||||||
|
LatLng(repeater.latitude!, repeater.longitude!),
|
||||||
|
) >
|
||||||
|
_maxRepeaterMatchDistanceMeters) {
|
||||||
|
return; //skip reapeaters that are far away from the last one with known GPS, to avoid false matches
|
||||||
|
}
|
||||||
|
for (var repeaterData in pathData) {
|
||||||
|
if (listEquals(
|
||||||
|
repeater.publicKey.sublist(0, 1),
|
||||||
|
Uint8List.fromList([repeaterData]),
|
||||||
|
)) {
|
||||||
|
pathContacts[repeaterData] = repeater;
|
||||||
|
lastContact = repeater;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
connector.contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
|
// For hops with no GPS contact, infer position from other contacts
|
||||||
for (var repeaterData in pathData) {
|
// with known GPS that share the same last-hop byte.
|
||||||
if (listEquals(
|
final Map<int, LatLng> inferredPositions = {};
|
||||||
repeater.publicKey.sublist(0, 1),
|
for (final hop in pathData) {
|
||||||
Uint8List.fromList([repeaterData]),
|
final contact = pathContacts[hop];
|
||||||
)) {
|
if (contact != null && contact.hasLocation) continue;
|
||||||
pathContacts[repeaterData] = repeater;
|
final peers = connector.contacts
|
||||||
|
.where(
|
||||||
|
(c) => c.hasLocation && c.path.isNotEmpty && c.path.last == hop,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
if (peers.isNotEmpty) {
|
||||||
|
final lat =
|
||||||
|
peers.map((c) => c.latitude!).reduce((a, b) => a + b) /
|
||||||
|
peers.length;
|
||||||
|
final lon =
|
||||||
|
peers.map((c) => c.longitude!).reduce((a, b) => a + b) /
|
||||||
|
peers.length;
|
||||||
|
inferredPositions[hop] = LatLng(lat, lon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
_hasData = true;
|
_hasData = true;
|
||||||
_traceData = PathTraceData(
|
_inferredHopPositions = inferredPositions;
|
||||||
pathData: pathData,
|
_traceData = PathTraceData(
|
||||||
snrData: snrData,
|
pathData: pathData,
|
||||||
pathContacts: pathContacts,
|
snrData: snrData,
|
||||||
);
|
pathContacts: pathContacts,
|
||||||
_points = <LatLng>[];
|
);
|
||||||
_points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
// Compute endpoint position for the target contact.
|
||||||
for (final hop in _traceData!.pathData) {
|
LatLng? targetPos;
|
||||||
final contact = _traceData!.pathContacts[hop];
|
bool targetGuessed = false;
|
||||||
if (contact != null &&
|
_targetContact = widget.targetContact;
|
||||||
contact.hasLocation &&
|
|
||||||
contact.latitude != null &&
|
if (_targetContact != null) {
|
||||||
contact.longitude != null) {
|
final tc = _targetContact!;
|
||||||
_points.add(LatLng(contact.latitude!, contact.longitude!));
|
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;
|
||||||
_polylines = _points.length > 1
|
_targetContactIsGuessed = targetGuessed;
|
||||||
? [
|
|
||||||
Polyline(
|
|
||||||
points: _points,
|
|
||||||
strokeWidth: 4,
|
|
||||||
color: Colors.blueAccent,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
: <Polyline>[];
|
|
||||||
|
|
||||||
_initialCenter = _points.isNotEmpty ? _points.first : const LatLng(0, 0);
|
_points = <LatLng>[];
|
||||||
_initialZoom = _points.isNotEmpty ? 13.0 : 2.0;
|
_points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
||||||
_bounds = _points.length > 1 ? LatLngBounds.fromPoints(_points) : null;
|
int hopLast = 0;
|
||||||
_mapKey = ValueKey(
|
int hopLastLast = 0;
|
||||||
'${context.l10n.pathTrace_you},${_formatPathPrefixes(_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];
|
||||||
|
if (contact != null && contact.hasLocation) {
|
||||||
|
_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
|
||||||
|
? [
|
||||||
|
Polyline(
|
||||||
|
points: _points,
|
||||||
|
strokeWidth: 4,
|
||||||
|
color: Colors.blueAccent,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: <Polyline>[];
|
||||||
|
|
||||||
|
_initialCenter = _points.isNotEmpty
|
||||||
|
? _points.first
|
||||||
|
: const LatLng(0, 0);
|
||||||
|
_initialZoom = _points.isNotEmpty ? 13.0 : 2.0;
|
||||||
|
_bounds = _points.length > 1 ? LatLngBounds.fromPoints(_points) : null;
|
||||||
|
_mapKey = ValueKey(
|
||||||
|
'${context.l10n.pathTrace_you},${_formatPathPrefixes(_traceData!.pathData)}',
|
||||||
|
);
|
||||||
|
_pathDistanceMeters = getPathDistanceMeters(_points);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
appLogger.error(
|
||||||
|
'Error handling trace response: $e',
|
||||||
|
tag: 'PathTraceMapScreen',
|
||||||
);
|
);
|
||||||
_pathDistanceMeters = getPathDistanceMeters(_points);
|
if (mounted) {
|
||||||
});
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_failed2Loaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<MeshCoreConnector>(
|
return Consumer<MeshCoreConnector>(
|
||||||
builder: (context, connector, _) {
|
builder: (context, connector, _) {
|
||||||
|
final settings = context.watch<AppSettingsService>().settings;
|
||||||
|
final isImperial = settings.unitSystem == UnitSystem.imperial;
|
||||||
final tileCache = context.read<MapTileCacheService>();
|
final tileCache = context.read<MapTileCacheService>();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -301,7 +517,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 &&
|
||||||
@@ -317,7 +534,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_hasData) _buildLegendCard(context, _traceData!),
|
if (_hasData)
|
||||||
|
_buildLegendCard(context, _traceData!, isImperial),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -326,55 +544,83 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Marker> _buildHopMarkers(List<int> pathData) {
|
List<Marker> _buildHopMarkers(
|
||||||
return [
|
List<int> pathData, {
|
||||||
for (final hop in pathData)
|
required bool showLabels,
|
||||||
if (_traceData!.pathContacts[hop] != null &&
|
required Contact? target,
|
||||||
_traceData!.pathContacts[hop]!.hasLocation)
|
}) {
|
||||||
Marker(
|
final markers = <Marker>[];
|
||||||
point: LatLng(
|
int hopLast = 0;
|
||||||
_traceData!.pathContacts[hop]!.latitude!,
|
int hopLastLast = 0;
|
||||||
_traceData!.pathContacts[hop]!.longitude!,
|
for (final hop in pathData) {
|
||||||
),
|
final contact = _traceData!.pathContacts[hop];
|
||||||
width: 35,
|
final inferred = _inferredHopPositions[hop];
|
||||||
height: 35,
|
final hasGps = contact != null && contact.hasLocation;
|
||||||
child: Container(
|
if (hop == hopLastLast && widget.flipPathAround) {
|
||||||
padding: const EdgeInsets.all(4),
|
continue; //skip duplicate hops in round-trip paths
|
||||||
decoration: BoxDecoration(
|
}
|
||||||
color: Colors.green,
|
if (!hasGps && inferred == null) {
|
||||||
shape: BoxShape.circle,
|
hopLastLast = hopLast;
|
||||||
border: Border.all(color: Colors.white, width: 2),
|
hopLast = hop;
|
||||||
boxShadow: [
|
continue; //skip hops with no GPS and no inferred position
|
||||||
BoxShadow(
|
}
|
||||||
color: Colors.black.withValues(alpha: 0.3),
|
final point = hasGps
|
||||||
blurRadius: 4,
|
? LatLng(contact.latitude!, contact.longitude!)
|
||||||
offset: const Offset(0, 2),
|
: inferred!;
|
||||||
),
|
final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||||
],
|
|
||||||
),
|
markers.add(
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Text(
|
|
||||||
_traceData!.pathContacts[hop]!.publicKey
|
|
||||||
.sublist(0, 1)
|
|
||||||
.map(
|
|
||||||
(b) => b.toRadixString(16).padLeft(2, '0').toUpperCase(),
|
|
||||||
)
|
|
||||||
.join(),
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (context.read<MeshCoreConnector>().selfLatitude != null &&
|
|
||||||
context.read<MeshCoreConnector>().selfLongitude != null)
|
|
||||||
Marker(
|
Marker(
|
||||||
point: LatLng(
|
point: point,
|
||||||
context.read<MeshCoreConnector>().selfLatitude!,
|
width: 35,
|
||||||
context.read<MeshCoreConnector>().selfLongitude!,
|
height: 35,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: hasGps
|
||||||
|
? Colors.green
|
||||||
|
: Colors.orange.withValues(alpha: 0.75),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.3),
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
hasGps ? label : '~$label',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (showLabels) {
|
||||||
|
markers.add(
|
||||||
|
_buildNodeLabelMarker(
|
||||||
|
point: point,
|
||||||
|
label: contact?.name ?? '~$label',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
hopLastLast = hopLast;
|
||||||
|
hopLast = hop;
|
||||||
|
}
|
||||||
|
|
||||||
|
final selfLat = context.read<MeshCoreConnector>().selfLatitude;
|
||||||
|
final selfLon = context.read<MeshCoreConnector>().selfLongitude;
|
||||||
|
if (selfLat != null && selfLon != null) {
|
||||||
|
final selfPoint = LatLng(selfLat, selfLon);
|
||||||
|
markers.add(
|
||||||
|
Marker(
|
||||||
|
point: selfPoint,
|
||||||
width: 35,
|
width: 35,
|
||||||
height: 35,
|
height: 35,
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -402,7 +648,94 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
);
|
||||||
|
if (showLabels) {
|
||||||
|
markers.add(
|
||||||
|
_buildNodeLabelMarker(
|
||||||
|
point: selfPoint,
|
||||||
|
label: context.l10n.pathTrace_you,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add target contact endpoint marker.
|
||||||
|
final targetPos = _targetContactPosition;
|
||||||
|
if (targetPos != null && 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
Marker _buildNodeLabelMarker({required LatLng point, required String label}) {
|
||||||
|
return Marker(
|
||||||
|
point: point,
|
||||||
|
width: 120,
|
||||||
|
height: 24,
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: Transform.translate(
|
||||||
|
offset: const Offset(0, -20),
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black54,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String formatDirectionText(PathTraceData pathTraceData, int index) {
|
String formatDirectionText(PathTraceData pathTraceData, int index) {
|
||||||
@@ -466,6 +799,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,
|
||||||
@@ -482,6 +816,14 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
),
|
),
|
||||||
minZoom: 2.0,
|
minZoom: 2.0,
|
||||||
maxZoom: 18.0,
|
maxZoom: 18.0,
|
||||||
|
onPositionChanged: (camera, hasGesture) {
|
||||||
|
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
||||||
|
if (shouldShow != _showNodeLabels && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_showNodeLabels = shouldShow;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
TileLayer(
|
TileLayer(
|
||||||
@@ -492,12 +834,22 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
),
|
),
|
||||||
if (_polylines.isNotEmpty) PolylineLayer(polylines: _polylines),
|
if (_polylines.isNotEmpty) PolylineLayer(polylines: _polylines),
|
||||||
if (_traceData!.pathData.isNotEmpty)
|
if (_traceData!.pathData.isNotEmpty)
|
||||||
MarkerLayer(markers: _buildHopMarkers(_traceData!.pathData)),
|
MarkerLayer(
|
||||||
|
markers: _buildHopMarkers(
|
||||||
|
_traceData!.pathData,
|
||||||
|
showLabels: _showNodeLabels,
|
||||||
|
target: target,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLegendCard(BuildContext context, PathTraceData pathTraceData) {
|
Widget _buildLegendCard(
|
||||||
|
BuildContext context,
|
||||||
|
PathTraceData pathTraceData,
|
||||||
|
bool isImperial,
|
||||||
|
) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
final maxHeight = MediaQuery.of(context).size.height * 0.35;
|
final maxHeight = MediaQuery.of(context).size.height * 0.35;
|
||||||
final estimatedHeight = 72.0 + (pathTraceData.pathData.length * 56.0);
|
final estimatedHeight = 72.0 + (pathTraceData.pathData.length * 56.0);
|
||||||
@@ -516,7 +868,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
child: Text(
|
child: Text(
|
||||||
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters)}',
|
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters, isImperial: isImperial)}',
|
||||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -532,6 +884,12 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
itemCount: pathTraceData.pathData.length + 1,
|
itemCount: pathTraceData.pathData.length + 1,
|
||||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
|
final snrUi = snrUiFromSNR(
|
||||||
|
index < pathTraceData.snrData.length
|
||||||
|
? pathTraceData.snrData[index]
|
||||||
|
: null,
|
||||||
|
context.read<MeshCoreConnector>().currentSf,
|
||||||
|
);
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -550,12 +908,22 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
),
|
),
|
||||||
style: const TextStyle(fontSize: 14),
|
style: const TextStyle(fontSize: 14),
|
||||||
),
|
),
|
||||||
trailing: SNRIcon(
|
trailing: Column(
|
||||||
snr:
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
pathTraceData.snrData[index].toSigned(
|
children: [
|
||||||
8,
|
Icon(
|
||||||
) /
|
snrUi.icon,
|
||||||
4.0,
|
color: snrUi.color,
|
||||||
|
size: 18.0,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
snrUi.text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: snrUi.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// Handle item tap
|
// Handle item tap
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user